
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.
394 lines
14 KiB
C++
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;
|
|
}
|
|
}
|