ladybird/Userland/Applications/PDFViewer/PDFViewerWidget.cpp
Rodrigo Tobar cb2cf6de99 PDFViewer: Perform standard error handling when opening files
The previous implementation of open_file had a lambda that was used to
inspect the call of ErrorOr-returning calls. This was a non-standard way
of doing this though, as the more usual and clearer way is to have an
inner function that returns ErrorOr, then handle any incoming errors on
the top level function.

This commit adds a try_open_file function, where all the logic occurs,
and all the failure-producing steps are simplied TRY'ed. The top level
open_file function takes that result and does what the lambda previously
did: showing a message box with the actual error.
2022-12-17 19:40:52 +01:00

389 lines
14 KiB
C++

/*
* Copyright (c) 2021-2022, Matthew Olsson <mattco@serenityos.org>
* Copyright (c) 2021, Mustafa Quraish <mustafa@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "PDFViewerWidget.h"
#include "AK/Assertions.h"
#include "AK/DeprecatedString.h"
#include "AK/Format.h"
#include "LibGUI/Forward.h"
#include <AK/HashMap.h>
#include <AK/HashTable.h>
#include <AK/Variant.h>
#include <LibCore/File.h>
#include <LibFileSystemAccessClient/Client.h>
#include <LibGUI/Application.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/FilePicker.h>
#include <LibGUI/Label.h>
#include <LibGUI/Menu.h>
#include <LibGUI/Menubar.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/Model.h>
#include <LibGUI/SortingProxyModel.h>
#include <LibGUI/Splitter.h>
#include <LibGUI/TableView.h>
#include <LibGUI/Toolbar.h>
#include <LibGUI/ToolbarContainer.h>
class PagedErrorsModel : public GUI::Model {
enum Columns {
Page = 0,
Message,
_Count
};
using PageErrors = AK::OrderedHashTable<DeprecatedString>;
using PagedErrors = HashMap<u32, PageErrors>;
public:
int row_count(GUI::ModelIndex const& index) const override
{
// There are two levels: number of pages and number of errors in page
if (!index.is_valid()) {
return static_cast<int>(m_paged_errors.size());
}
if (!index.parent().is_valid()) {
auto errors_in_page = m_paged_errors.get(index.row()).release_value().size();
return static_cast<int>(errors_in_page);
}
return 0;
}
int column_count(GUI::ModelIndex const&) const override
{
return Columns::_Count;
}
int tree_column() const override
{
return Columns::Page;
}
DeprecatedString column_name(int index) const override
{
switch (index) {
case 0:
return "Page";
case 1:
return "Message";
default:
VERIFY_NOT_REACHED();
}
}
GUI::ModelIndex index(int row, int column, GUI::ModelIndex const& parent) const override
{
if (!parent.is_valid()) {
return create_index(row, column);
}
auto const& page = m_pages_with_errors[parent.row()];
return create_index(row, column, &page);
}
GUI::ModelIndex parent_index(GUI::ModelIndex const& index) const override
{
auto* const internal_data = index.internal_data();
if (internal_data == nullptr)
return {};
auto page = *static_cast<u32 const*>(internal_data);
auto page_idx = static_cast<int>(m_pages_with_errors.find_first_index(page).release_value());
return create_index(page_idx, index.column());
}
virtual GUI::Variant data(GUI::ModelIndex const& index, GUI::ModelRole) const override
{
if (!index.parent().is_valid()) {
switch (index.column()) {
case Columns::Page:
return m_pages_with_errors[index.row()] + 1;
case Columns::Message:
return DeprecatedString::formatted("{} errors", m_paged_errors.get(index.row()).release_value().size());
default:
VERIFY_NOT_REACHED();
}
}
auto page = *static_cast<u32 const*>(index.internal_data());
switch (index.column()) {
case Columns::Page:
return "";
case Columns::Message: {
auto page_errors = m_paged_errors.get(page).release_value();
// dbgln("Errors on page {}: {}. Requesting data for index {},{}", page, page_errors.size(), index.row(), index.column());
auto it = page_errors.begin();
auto row = index.row();
for (int i = 0; i < row; ++i, ++it)
;
return *it;
}
}
VERIFY_NOT_REACHED();
}
void add_errors(u32 page, PDF::Errors const& errors)
{
auto old_size = total_error_count();
if (!m_pages_with_errors.contains_slow(page)) {
m_pages_with_errors.append(page);
}
auto& page_errors = m_paged_errors.ensure(page);
for (auto const& error : errors.errors())
page_errors.set(error.message());
auto new_size = total_error_count();
if (old_size != new_size)
invalidate();
}
private:
size_t total_error_count() const
{
size_t count = 0;
for (auto const& entry : m_paged_errors)
count += entry.value.size();
return count;
}
Vector<u32> m_pages_with_errors;
PagedErrors m_paged_errors;
};
PDFViewerWidget::PDFViewerWidget()
: m_paged_errors_model(adopt_ref(*new PagedErrorsModel()))
{
set_fill_with_background_color(true);
set_layout<GUI::VerticalBoxLayout>();
auto& toolbar_container = add<GUI::ToolbarContainer>();
auto& toolbar = toolbar_container.add<GUI::Toolbar>();
auto& h_splitter = add<GUI::HorizontalSplitter>();
h_splitter.layout()->set_spacing(4);
m_sidebar = h_splitter.add<SidebarWidget>();
m_sidebar->set_preferred_width(200);
m_sidebar->set_visible(false);
auto& v_splitter = h_splitter.add<GUI::VerticalSplitter>();
v_splitter.layout()->set_spacing(4);
m_viewer = v_splitter.add<PDFViewer>();
m_viewer->on_page_change = [&](auto new_page) {
m_page_text_box->set_current_number(new_page + 1, GUI::AllowCallback::No);
m_go_to_prev_page_action->set_enabled(new_page > 0);
m_go_to_next_page_action->set_enabled(new_page < m_viewer->document()->get_page_count() - 1);
};
m_viewer->on_render_errors = [&](u32 page, PDF::Errors const& errors) {
verify_cast<PagedErrorsModel>(m_paged_errors_model.ptr())->add_errors(page, errors);
};
m_errors_tree_view = v_splitter.add<GUI::TreeView>();
m_errors_tree_view->set_preferred_height(10);
m_errors_tree_view->column_header().set_visible(true);
m_errors_tree_view->set_should_fill_selected_rows(true);
m_errors_tree_view->set_selection_behavior(GUI::AbstractView::SelectionBehavior::SelectRows);
m_errors_tree_view->set_model(MUST(GUI::SortingProxyModel::create(m_paged_errors_model)));
m_errors_tree_view->set_key_column(0);
initialize_toolbar(toolbar);
}
void PDFViewerWidget::initialize_menubar(GUI::Window& window)
{
auto& file_menu = window.add_menu("&File");
file_menu.add_action(GUI::CommonActions::make_open_action([&](auto&) {
auto response = FileSystemAccessClient::Client::the().try_open_file(&window);
if (!response.is_error())
open_file(*response.value());
}));
file_menu.add_separator();
file_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
GUI::Application::the()->quit();
}));
auto& view_menu = window.add_menu("&View");
view_menu.add_action(*m_toggle_sidebar_action);
view_menu.add_separator();
auto& view_mode_menu = view_menu.add_submenu("View &Mode");
view_mode_menu.add_action(*m_page_view_mode_single);
view_mode_menu.add_action(*m_page_view_mode_multiple);
view_menu.add_separator();
view_menu.add_action(*m_zoom_in_action);
view_menu.add_action(*m_zoom_out_action);
view_menu.add_action(*m_reset_zoom_action);
auto& help_menu = window.add_menu("&Help");
help_menu.add_action(GUI::CommonActions::make_command_palette_action(&window));
help_menu.add_action(GUI::CommonActions::make_about_action("PDF Viewer", GUI::Icon::default_icon("app-pdf-viewer"sv), &window));
}
void PDFViewerWidget::initialize_toolbar(GUI::Toolbar& toolbar)
{
auto open_outline_action = GUI::Action::create(
"Toggle &Sidebar", { Mod_Ctrl, Key_S }, Gfx::Bitmap::try_load_from_file("/res/icons/16x16/sidebar.png"sv).release_value_but_fixme_should_propagate_errors(), [&](auto&) {
m_sidebar_open = !m_sidebar_open;
m_sidebar->set_visible(m_sidebar_open);
},
nullptr);
open_outline_action->set_enabled(false);
m_toggle_sidebar_action = open_outline_action;
toolbar.add_action(*open_outline_action);
toolbar.add_separator();
m_go_to_prev_page_action = GUI::Action::create("Go to &Previous Page", Gfx::Bitmap::try_load_from_file("/res/icons/16x16/go-up.png"sv).release_value_but_fixme_should_propagate_errors(), [&](auto&) {
VERIFY(m_viewer->current_page() > 0);
m_page_text_box->set_current_number(m_viewer->current_page());
});
m_go_to_prev_page_action->set_enabled(false);
m_go_to_next_page_action = GUI::Action::create("Go to &Next Page", Gfx::Bitmap::try_load_from_file("/res/icons/16x16/go-down.png"sv).release_value_but_fixme_should_propagate_errors(), [&](auto&) {
VERIFY(m_viewer->current_page() < m_viewer->document()->get_page_count() - 1);
m_page_text_box->set_current_number(m_viewer->current_page() + 2);
});
m_go_to_next_page_action->set_enabled(false);
toolbar.add_action(*m_go_to_prev_page_action);
toolbar.add_action(*m_go_to_next_page_action);
m_page_text_box = toolbar.add<NumericInput>();
m_page_text_box->set_enabled(false);
m_page_text_box->set_fixed_width(30);
m_page_text_box->set_min_number(1);
m_page_text_box->on_number_changed = [&](i32 number) {
auto page_count = m_viewer->document()->get_page_count();
auto new_page_number = static_cast<u32>(number);
VERIFY(new_page_number >= 1 && new_page_number <= page_count);
m_viewer->set_current_page(new_page_number - 1);
m_go_to_prev_page_action->set_enabled(new_page_number > 1);
m_go_to_next_page_action->set_enabled(new_page_number < page_count);
};
m_total_page_label = toolbar.add<GUI::Label>();
m_total_page_label->set_autosize(true, 5);
toolbar.add_separator();
m_zoom_in_action = GUI::CommonActions::make_zoom_in_action([&](auto&) {
m_viewer->zoom_in();
});
m_zoom_out_action = GUI::CommonActions::make_zoom_out_action([&](auto&) {
m_viewer->zoom_out();
});
m_reset_zoom_action = GUI::CommonActions::make_reset_zoom_action([&](auto&) {
m_viewer->reset_zoom();
});
m_rotate_counterclockwise_action = GUI::CommonActions::make_rotate_counterclockwise_action([&](auto&) {
m_viewer->rotate(-90);
});
m_rotate_clockwise_action = GUI::CommonActions::make_rotate_clockwise_action([&](auto&) {
m_viewer->rotate(90);
});
m_zoom_in_action->set_enabled(false);
m_zoom_out_action->set_enabled(false);
m_reset_zoom_action->set_enabled(false);
m_rotate_counterclockwise_action->set_enabled(false);
m_rotate_clockwise_action->set_enabled(false);
m_page_view_mode_single = GUI::Action::create_checkable("Single", [&](auto&) {
m_viewer->set_page_view_mode(PDFViewer::PageViewMode::Single);
});
m_page_view_mode_single->set_status_tip("Show single page at a time");
m_page_view_mode_multiple = GUI::Action::create_checkable("Multiple", [&](auto&) {
m_viewer->set_page_view_mode(PDFViewer::PageViewMode::Multiple);
});
m_page_view_mode_multiple->set_status_tip("Show multiple pages at a time");
if (m_viewer->page_view_mode() == PDFViewer::PageViewMode::Single) {
m_page_view_mode_single->set_checked(true);
} else {
m_page_view_mode_multiple->set_checked(true);
}
m_page_view_action_group.add_action(*m_page_view_mode_single);
m_page_view_action_group.add_action(*m_page_view_mode_multiple);
m_page_view_action_group.set_exclusive(true);
toolbar.add_action(*m_page_view_mode_single);
toolbar.add_action(*m_page_view_mode_multiple);
toolbar.add_separator();
toolbar.add_action(*m_zoom_in_action);
toolbar.add_action(*m_zoom_out_action);
toolbar.add_action(*m_reset_zoom_action);
toolbar.add_action(*m_rotate_counterclockwise_action);
toolbar.add_action(*m_rotate_clockwise_action);
toolbar.add_separator();
m_show_clipping_paths = toolbar.add<GUI::CheckBox>();
m_show_clipping_paths->set_text("Show clipping paths");
m_show_clipping_paths->set_checked(m_viewer->show_clipping_paths(), GUI::AllowCallback::No);
m_show_clipping_paths->on_checked = [&](auto checked) { m_viewer->set_show_clipping_paths(checked); };
m_show_images = toolbar.add<GUI::CheckBox>();
m_show_images->set_text("Show images");
m_show_images->set_checked(m_viewer->show_images(), GUI::AllowCallback::No);
m_show_images->on_checked = [&](auto checked) { m_viewer->set_show_images(checked); };
}
void PDFViewerWidget::open_file(Core::File& file)
{
auto maybe_error = try_open_file(file);
if (maybe_error.is_error()) {
auto error = maybe_error.release_error();
warnln("{}", error.message());
GUI::MessageBox::show_error(nullptr, "Failed to load the document."sv);
}
}
PDF::PDFErrorOr<void> PDFViewerWidget::try_open_file(Core::File& file)
{
window()->set_title(DeprecatedString::formatted("{} - PDF Viewer", file.filename()));
m_buffer = file.read_all();
auto document = TRY(PDF::Document::create(m_buffer));
if (auto sh = document->security_handler(); sh && !sh->has_user_password()) {
// FIXME: Prompt the user for a password
VERIFY_NOT_REACHED();
}
TRY(document->initialize());
TRY(m_viewer->set_document(document));
m_total_page_label->set_text(DeprecatedString::formatted("of {}", document->get_page_count()));
m_page_text_box->set_enabled(true);
m_page_text_box->set_current_number(1, GUI::AllowCallback::No);
m_page_text_box->set_max_number(document->get_page_count());
m_go_to_prev_page_action->set_enabled(false);
m_go_to_next_page_action->set_enabled(document->get_page_count() > 1);
m_toggle_sidebar_action->set_enabled(true);
m_zoom_in_action->set_enabled(true);
m_zoom_out_action->set_enabled(true);
m_reset_zoom_action->set_enabled(true);
m_rotate_counterclockwise_action->set_enabled(true);
m_rotate_clockwise_action->set_enabled(true);
m_show_clipping_paths->set_checked(m_viewer->show_clipping_paths(), GUI::AllowCallback::No);
if (document->outline()) {
auto outline = document->outline();
m_sidebar->set_outline(outline.release_nonnull());
m_sidebar->set_visible(true);
m_sidebar_open = true;
} else {
m_sidebar->set_outline({});
m_sidebar->set_visible(false);
m_sidebar_open = false;
}
return {};
}