ladybird/Userland/Applications/PDFViewer/PDFViewerWidget.cpp
Rodrigo Tobar d4ecdf3ced PDFViewer: Avoid errors due to copying of ErrorOr
The handle_error took PDFErrorOr<T> objects by value, meaning that their
inner values (the error or value stored in the underlying Variant) were
somehow copied over. In the first instance where this lambda is called
with T = NonnullRefPtr, resulting in funky behavior (invalid
NonnullRefPtr state with a VALIDATE fail): if there is no error then the
PDFErrorOr<T> copy is destroyed, which might be causing the underlying
NonnullRefPtr to be destroyed, but somehow the original in the caller
context gets affected and fails verification.

The solution seems simple anyway: just pass the value by reference
(lvalue or rvalue) so the original object can be used directly, avoiding
destruction.
2022-12-16 10:04:23 +01:00

394 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)
{
window()->set_title(DeprecatedString::formatted("{} - PDF Viewer", file.filename()));
auto handle_error = [&](auto&& maybe_error) {
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);
return true;
}
return false;
};
m_buffer = file.read_all();
auto maybe_document = PDF::Document::create(m_buffer);
if (handle_error(maybe_document))
return;
auto document = maybe_document.release_value();
if (auto sh = document->security_handler(); sh && !sh->has_user_password()) {
// FIXME: Prompt the user for a password
VERIFY_NOT_REACHED();
}
if (handle_error(document->initialize()))
return;
if (handle_error(m_viewer->set_document(document)))
return;
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;
}
}