PDF4QT/Pdf4QtPageMaster/mainwindow.cpp

1087 lines
42 KiB
C++

// Copyright (C) 2021 Jakub Melka
//
// This file is part of PDF4QT.
//
// PDF4QT is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// with the written consent of the copyright owner, any later version.
//
// PDF4QT is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with PDF4QT. If not, see <https://www.gnu.org/licenses/>.
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "aboutdialog.h"
#include "assembleoutputsettingsdialog.h"
#include "selectoutlinetoregroupdialog.h"
#include "pdfaction.h"
#include "pdfwidgetutils.h"
#include "pdfdocumentreader.h"
#include "pdfdocumentwriter.h"
#include <QFileDialog>
#include <QMessageBox>
#include <QInputDialog>
#include <QClipboard>
#include <QToolBar>
#include <QDesktopServices>
#include <QImageReader>
#include <QPixmapCache>
#include <QScreen>
#include <QGuiApplication>
#include <QDragEnterEvent>
#include <QDropEvent>
#include <QSettings>
#include <QMimeData>
namespace pdfpagemaster
{
MainWindow::MainWindow(QWidget* parent) :
QMainWindow(parent),
ui(new Ui::MainWindow),
m_model(new PageItemModel(this)),
m_delegate(new PageItemDelegate(m_model, this)),
m_dropAction(Qt::IgnoreAction)
{
ui->setupUi(this);
m_delegate->setPageImageSize(getDefaultPageImageSize());
ui->documentItemsView->setModel(m_model);
ui->documentItemsView->setItemDelegate(m_delegate);
connect(ui->documentItemsView, &QListView::customContextMenuRequested, this, &MainWindow::onWorkspaceCustomContextMenuRequested);
setMinimumSize(pdf::PDFWidgetUtils::scaleDPI(this, QSize(800, 600)));
ui->actionClear->setData(int(Operation::Clear));
ui->actionCloneSelection->setData(int(Operation::CloneSelection));
ui->actionRemoveSelection->setData(int(Operation::RemoveSelection));
ui->actionReplaceSelection->setData(int(Operation::ReplaceSelection));
ui->actionRestoreRemovedItems->setData(int(Operation::RestoreRemovedItems));
ui->actionUndo->setData(int(Operation::Undo));
ui->actionRedo->setData(int(Operation::Redo));
ui->actionCut->setData(int(Operation::Cut));
ui->actionCopy->setData(int(Operation::Copy));
ui->actionPaste->setData(int(Operation::Paste));
ui->actionRotate_Left->setData(int(Operation::RotateLeft));
ui->actionRotate_Right->setData(int(Operation::RotateRight));
ui->actionGroup->setData(int(Operation::Group));
ui->actionUngroup->setData(int(Operation::Ungroup));
ui->actionSelect_None->setData(int(Operation::SelectNone));
ui->actionSelect_All->setData(int(Operation::SelectAll));
ui->actionSelect_Even->setData(int(Operation::SelectEven));
ui->actionSelect_Odd->setData(int(Operation::SelectOdd));
ui->actionSelect_Portrait->setData(int(Operation::SelectPortrait));
ui->actionSelect_Landscape->setData(int(Operation::SelectLandscape));
ui->actionZoom_In->setData(int(Operation::ZoomIn));
ui->actionZoom_Out->setData(int(Operation::ZoomOut));
ui->actionUnited_Document->setData(int(Operation::Unite));
ui->actionSeparate_to_Multiple_Documents->setData(int(Operation::Separate));
ui->actionSeparate_to_Multiple_Documents_Grouped->setData(int(Operation::SeparateGrouped));
ui->actionInsert_Image->setData(int(Operation::InsertImage));
ui->actionInsert_Empty_Page->setData(int(Operation::InsertEmptyPage));
ui->actionInsert_PDF->setData(int(Operation::InsertPDF));
ui->actionGet_Source->setData(int(Operation::GetSource));
ui->actionBecomeASponsor->setData(int(Operation::BecomeSponsor));
ui->actionAbout->setData(int(Operation::About));
ui->actionInvert_Selection->setData(int(Operation::InvertSelection));
ui->actionRegroup_Even_Odd->setData(int(Operation::RegroupEvenOdd));
ui->actionRegroup_by_Page_Pairs->setData(int(Operation::RegroupPaired));
ui->actionRegroup_by_Outline->setData(int(Operation::RegroupOutline));
ui->actionRegroup_by_Alternating_Pages->setData(int(Operation::RegroupAlternatingPages));
ui->actionRegroup_by_Alternating_Pages_Reversed_Order->setData(int(Operation::RegroupAlternatingPagesReversed));
ui->actionPrepare_Icon_Theme->setData(int(Operation::PrepareIconTheme));
m_iconTheme.registerAction(ui->actionAddDocuments, ":/pdfpagemaster/resources/open.svg");
m_iconTheme.registerAction(ui->actionClose, ":/pdfpagemaster/resources/close.svg");
m_iconTheme.registerAction(ui->actionCloneSelection, ":/pdfpagemaster/resources/clone-selection.svg");
m_iconTheme.registerAction(ui->actionRemoveSelection, ":/pdfpagemaster/resources/remove-selection.svg");
m_iconTheme.registerAction(ui->actionRestoreRemovedItems, ":/pdfpagemaster/resources/restore-removed-items.svg");
m_iconTheme.registerAction(ui->actionInsert_PDF, ":/pdfpagemaster/resources/insert-page-from-pdf.svg");
m_iconTheme.registerAction(ui->actionInsert_Image, ":/pdfpagemaster/resources/insert-image.svg");
m_iconTheme.registerAction(ui->actionInsert_Empty_Page, ":/pdfpagemaster/resources/insert-empty-page.svg");
m_iconTheme.registerAction(ui->actionCut, ":/pdfpagemaster/resources/cut.svg");
m_iconTheme.registerAction(ui->actionCopy, ":/pdfpagemaster/resources/copy.svg");
m_iconTheme.registerAction(ui->actionPaste, ":/pdfpagemaster/resources/paste.svg");
m_iconTheme.registerAction(ui->actionReplaceSelection, ":/pdfpagemaster/resources/replace-selection.svg");
m_iconTheme.registerAction(ui->actionSelect_None, ":/pdfpagemaster/resources/select-none.svg");
m_iconTheme.registerAction(ui->actionSelect_All, ":/pdfpagemaster/resources/select-all.svg");
m_iconTheme.registerAction(ui->actionSelect_Even, ":/pdfpagemaster/resources/select-even.svg");
m_iconTheme.registerAction(ui->actionSelect_Odd, ":/pdfpagemaster/resources/select-odd.svg");
m_iconTheme.registerAction(ui->actionSelect_Portrait, ":/pdfpagemaster/resources/select-portrait.svg");
m_iconTheme.registerAction(ui->actionSelect_Landscape, ":/pdfpagemaster/resources/select-landscape.svg");
m_iconTheme.registerAction(ui->actionRotate_Right, ":/pdfpagemaster/resources/rotate-right.svg");
m_iconTheme.registerAction(ui->actionRotate_Left, ":/pdfpagemaster/resources/rotate-left.svg");
m_iconTheme.registerAction(ui->actionZoom_In, ":/pdfpagemaster/resources/zoom-in.svg");
m_iconTheme.registerAction(ui->actionZoom_Out, ":/pdfpagemaster/resources/zoom-out.svg");
m_iconTheme.registerAction(ui->actionGet_Source, ":/pdfpagemaster/resources/get-source.svg");
m_iconTheme.registerAction(ui->actionBecomeASponsor, ":/pdfpagemaster/resources/wallet.svg");
m_iconTheme.registerAction(ui->actionAbout, ":/pdfpagemaster/resources/about.svg");
m_iconTheme.registerAction(ui->actionUnited_Document, ":/pdfpagemaster/resources/make-united-document.svg");
m_iconTheme.registerAction(ui->actionSeparate_to_Multiple_Documents, ":/pdfpagemaster/resources/make-separated-document.svg");
m_iconTheme.registerAction(ui->actionSeparate_to_Multiple_Documents_Grouped, ":/pdfpagemaster/resources/make-separated-document-from-groups.svg");
m_iconTheme.registerAction(ui->actionGroup, ":/pdfpagemaster/resources/group.svg");
m_iconTheme.registerAction(ui->actionUngroup, ":/pdfpagemaster/resources/ungroup.svg");
m_iconTheme.registerAction(ui->actionClear, ":/pdfpagemaster/resources/clear.svg");
m_iconTheme.registerAction(ui->actionRegroup_Even_Odd, ":/pdfpagemaster/resources/regroup-even-odd.svg");
m_iconTheme.registerAction(ui->actionRegroup_by_Page_Pairs, ":/pdfpagemaster/resources/regroup-pairs.svg");
m_iconTheme.registerAction(ui->actionRegroup_by_Outline, ":/pdfpagemaster/resources/regroup-outline.svg");
m_iconTheme.registerAction(ui->actionRegroup_by_Alternating_Pages, ":/pdfpagemaster/resources/regroup-alternating.svg");
m_iconTheme.registerAction(ui->actionRegroup_by_Alternating_Pages_Reversed_Order, ":/pdfpagemaster/resources/regroup-alternating-reversed.svg");
m_iconTheme.registerAction(ui->actionInvert_Selection, ":/pdfpagemaster/resources/invert-selection.svg");
m_iconTheme.registerAction(ui->actionUndo, ":/pdfpagemaster/resources/undo.svg");
m_iconTheme.registerAction(ui->actionRedo, ":/pdfpagemaster/resources/redo.svg");
m_iconTheme.setDirectory("pdfpagemaster_theme");
m_iconTheme.setPrefix(":/pdfpagemaster/resources/");
m_iconTheme.loadTheme();
QToolBar* mainToolbar = addToolBar(tr("&Main"));
mainToolbar->setObjectName("main_toolbar");
mainToolbar->addAction(ui->actionAddDocuments);
mainToolbar->addSeparator();
mainToolbar->addActions({ ui->actionCloneSelection, ui->actionRemoveSelection });
mainToolbar->addSeparator();
mainToolbar->addActions({ ui->actionUndo, ui->actionRedo });
mainToolbar->addSeparator();
mainToolbar->addActions({ ui->actionCut, ui->actionCopy, ui->actionPaste });
mainToolbar->addSeparator();
mainToolbar->addActions({ ui->actionGroup, ui->actionUngroup });
QToolBar* insertToolbar = addToolBar(tr("&Insert"));
insertToolbar->setObjectName("insert_toolbar");
insertToolbar->addActions({ ui->actionInsert_PDF, ui->actionInsert_Image, ui->actionInsert_Empty_Page });
QToolBar* selectToolbar = addToolBar(tr("&Select"));
selectToolbar->setObjectName("select_toolbar");
selectToolbar->addActions({ ui->actionSelect_None, ui->actionSelect_All, ui->actionSelect_Even, ui->actionSelect_Odd, ui->actionSelect_Portrait, ui->actionSelect_Landscape, ui->actionInvert_Selection });
QToolBar* regroupToolbar = addToolBar(tr("&Regroup"));
regroupToolbar->setObjectName("regroup_toolbar");
regroupToolbar->addActions({ ui->actionRegroup_Even_Odd, ui->actionRegroup_by_Page_Pairs, ui->actionRegroup_by_Outline, ui->actionRegroup_by_Alternating_Pages, ui->actionRegroup_by_Alternating_Pages_Reversed_Order });
QToolBar* zoomToolbar = addToolBar(tr("&Zoom"));
zoomToolbar->setObjectName("zoom_toolbar");
zoomToolbar->addActions({ ui->actionZoom_In, ui->actionZoom_Out });
QToolBar* makeToolbar = addToolBar(tr("Ma&ke"));
makeToolbar->setObjectName("make_toolbar");
makeToolbar->addActions({ ui->actionUnited_Document, ui->actionSeparate_to_Multiple_Documents, ui->actionSeparate_to_Multiple_Documents_Grouped });
QSize iconSize = pdf::PDFWidgetUtils::scaleDPI(this, QSize(24, 24));
auto toolbars = findChildren<QToolBar*>();
for (QToolBar* toolbar : toolbars)
{
toolbar->setIconSize(iconSize);
ui->menuToolbars->addAction(toolbar->toggleViewAction());
}
connect(&m_mapper, &QSignalMapper::mappedInt, this, &MainWindow::onMappedActionTriggered);
connect(ui->documentItemsView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::updateActions);
QList<QAction*> actions = findChildren<QAction*>();
for (QAction* action : actions)
{
QVariant actionData = action->data();
if (actionData.isValid())
{
connect(action, &QAction::triggered, &m_mapper, QOverload<>::of(&QSignalMapper::map));
m_mapper.setMapping(action, actionData.toInt());
}
}
if (pdf::PDFWidgetUtils::isDarkTheme())
{
pdf::PDFWidgetUtils::convertActionsForDarkTheme(actions, iconSize, qGuiApp->devicePixelRatio());
}
// Initialize pixmap cache size
const int depth = 4; // 4 bytes (ARGB)
const int reserveSize = 2; // Caching of two screens
QSize size = QGuiApplication::primaryScreen()->availableVirtualSize();
int bytes = size.width() * size.height() * depth * reserveSize;
int kBytes = bytes / 1024;
QPixmapCache::setCacheLimit(kBytes);
loadSettings();
updateActions();
#ifndef NDEBUG
pdf::PDFWidgetUtils::checkMenuAccessibility(this);
#endif
}
MainWindow::~MainWindow()
{
saveSettings();
delete ui;
}
QSize MainWindow::getMinPageImageSize() const
{
return pdf::PDFWidgetUtils::scaleDPI(this, QSize(40, 40));
}
QSize MainWindow::getDefaultPageImageSize() const
{
return pdf::PDFWidgetUtils::scaleDPI(this, QSize(100, 100));
}
QSize MainWindow::getMaxPageImageSize() const
{
return pdf::PDFWidgetUtils::scaleDPI(this, QSize(250, 250));
}
void MainWindow::resizeEvent(QResizeEvent* resizeEvent)
{
QMainWindow::resizeEvent(resizeEvent);
ui->documentItemsView->doItemsLayout();
}
void MainWindow::on_actionClose_triggered()
{
close();
}
void MainWindow::on_actionAddDocuments_triggered()
{
QStringList fileNames = QFileDialog::getOpenFileNames(this, tr("Select PDF document(s)"), m_settings.directory, tr("PDF document (*.pdf)"));
if (!fileNames.isEmpty())
{
for (const QString& fileName : fileNames)
{
if (!insertDocument(fileName, QModelIndex()))
{
break;
}
}
}
}
void MainWindow::onMappedActionTriggered(int actionId)
{
performOperation(static_cast<Operation>(actionId));
}
void MainWindow::onWorkspaceCustomContextMenuRequested(const QPoint& point)
{
QMenu* contextMenu = new QMenu(this);
contextMenu->addAction(ui->actionCut);
contextMenu->addAction(ui->actionCopy);
contextMenu->addAction(ui->actionPaste);
contextMenu->addSeparator();
contextMenu->addAction(ui->actionRotate_Left);
contextMenu->addAction(ui->actionRotate_Right);
QMenu* selectMenu = contextMenu->addMenu(tr("Select"));
selectMenu->addAction(ui->actionSelect_All);
selectMenu->addAction(ui->actionSelect_Even);
selectMenu->addAction(ui->actionSelect_Odd);
selectMenu->addAction(ui->actionSelect_Landscape);
selectMenu->addAction(ui->actionSelect_Portrait);
selectMenu->addAction(ui->actionSelect_None);
QMenu* regroupMenu = contextMenu->addMenu(tr("Regroup"));
regroupMenu->addAction(ui->actionRegroup_Even_Odd);
regroupMenu->addAction(ui->actionRegroup_by_Alternating_Pages);
regroupMenu->addAction(ui->actionRegroup_by_Alternating_Pages_Reversed_Order);
regroupMenu->addAction(ui->actionRegroup_by_Page_Pairs);
regroupMenu->addAction(ui->actionRegroup_by_Outline);
contextMenu->addSeparator();
contextMenu->addAction(ui->actionGroup);
contextMenu->addAction(ui->actionUngroup);
contextMenu->exec(ui->documentItemsView->viewport()->mapToGlobal(point));
}
void MainWindow::updateActions()
{
QList<QAction*> actions = findChildren<QAction*>();
for (QAction* action : actions)
{
QVariant actionData = action->data();
if (actionData.isValid())
{
action->setEnabled(canPerformOperation(static_cast<Operation>(actionData.toInt())));
}
}
}
void MainWindow::loadSettings()
{
QSettings settings(QSettings::IniFormat, QSettings::UserScope, QCoreApplication::organizationName(), QCoreApplication::applicationName());
settings.beginGroup("MainWindow");
QByteArray geometry = settings.value("geometry", QByteArray()).toByteArray();
if (geometry.isEmpty())
{
QRect availableGeometry = QApplication::primaryScreen()->availableGeometry();
QRect windowRect(0, 0, availableGeometry.width() / 2, availableGeometry.height() / 2);
windowRect = windowRect.translated(availableGeometry.center() - windowRect.center());
setGeometry(windowRect);
}
else
{
restoreGeometry(geometry);
}
QByteArray state = settings.value("windowState", QByteArray()).toByteArray();
if (!state.isEmpty())
{
restoreState(state);
}
settings.endGroup();
settings.beginGroup("Settings");
m_settings.directory = settings.value("directory").toString();
settings.endGroup();
}
void MainWindow::saveSettings()
{
QSettings settings(QSettings::IniFormat, QSettings::UserScope, QCoreApplication::organizationName(), QCoreApplication::applicationName());
settings.beginGroup("MainWindow");
settings.setValue("geometry", saveGeometry());
settings.setValue("windowState", saveState());
settings.endGroup();
settings.beginGroup("Settings");
settings.setValue("directory", m_settings.directory);
settings.endGroup();
}
bool MainWindow::insertDocument(const QString& fileName, const QModelIndex& insertIndex)
{
bool isDocumentInserted = true;
QFileInfo fileInfo(fileName);
auto queryPassword = [this, &fileInfo](bool* ok)
{
*ok = false;
return QInputDialog::getText(this, tr("Encrypted document"), tr("Enter password to access document '%1'").arg(fileInfo.fileName()), QLineEdit::Password, QString(), ok);
};
// Mark current directory as this
m_settings.directory = fileInfo.dir().absolutePath();
// Try to open a new document
pdf::PDFDocumentReader reader(nullptr, qMove(queryPassword), true, false);
pdf::PDFDocument document = reader.readFromFile(fileName);
QString errorMessage = reader.getErrorMessage();
pdf::PDFDocumentReader::Result result = reader.getReadingResult();
if (result == pdf::PDFDocumentReader::Result::OK)
{
const pdf::PDFSecurityHandler* securityHandler = document.getStorage().getSecurityHandler();
if (securityHandler->isAllowed(pdf::PDFSecurityHandler::Permission::Assemble) ||
securityHandler->isAllowed(pdf::PDFSecurityHandler::Permission::Modify))
{
m_model->insertDocument(fileName, qMove(document), insertIndex);
}
else
{
isDocumentInserted = false;
QMessageBox::critical(this, tr("Error"), tr("Document security doesn't permit to organize pages."));
}
}
else if (result == pdf::PDFDocumentReader::Result::Failed)
{
isDocumentInserted = false;
QMessageBox::critical(this, tr("Error"), errorMessage);
}
else if (result == pdf::PDFDocumentReader::Result::Cancelled)
{
isDocumentInserted = false;
}
updateActions();
return isDocumentInserted;
}
bool MainWindow::canPerformOperation(Operation operation) const
{
QModelIndexList selection = ui->documentItemsView->selectionModel()->selection().indexes();
const bool isSelected = !selection.isEmpty();
const bool isMultiSelected = selection.size() > 1;
const bool isModelEmpty = m_model->rowCount(QModelIndex()) == 0;
switch (operation)
{
case Operation::Clear:
return true;
case Operation::CloneSelection:
case Operation::RemoveSelection:
case Operation::ReplaceSelection:
return isSelected;
case Operation::RestoreRemovedItems:
return !m_model->isTrashBinEmpty();
case Operation::Undo:
return m_model->canUndo();
case Operation::Redo:
return m_model->canRedo();
case Operation::Cut:
case Operation::Copy:
return isSelected;
case Operation::Paste:
{
const QMimeData* mimeData = QApplication::clipboard()->mimeData();
return mimeData && mimeData->hasFormat(PageItemModel::getMimeDataType());
}
case Operation::RotateLeft:
case Operation::RotateRight:
return isSelected;
case Operation::Group:
return isMultiSelected;
case Operation::Ungroup:
return m_model->isGrouped(selection);
case Operation::SelectNone:
return isSelected;
case Operation::SelectAll:
case Operation::SelectEven:
case Operation::SelectOdd:
case Operation::SelectPortrait:
case Operation::SelectLandscape:
return !isModelEmpty;
case Operation::ZoomIn:
return m_delegate->getPageImageSize() != getMaxPageImageSize();
case Operation::ZoomOut:
return m_delegate->getPageImageSize() != getMinPageImageSize();
case Operation::Unite:
case Operation::Separate:
case Operation::SeparateGrouped:
return !isModelEmpty;
case Operation::InsertImage:
case Operation::InsertEmptyPage:
case Operation::InsertPDF:
case Operation::GetSource:
case Operation::BecomeSponsor:
case Operation::About:
case Operation::PrepareIconTheme:
return true;
case Operation::InvertSelection:
return !isModelEmpty;
case Operation::RegroupEvenOdd:
{
PageItemModel::SelectionInfo info = m_model->getSelectionInfo(selection);
return info.isDocumentOnly();
}
case Operation::RegroupPaired:
return !isModelEmpty && !selection.isEmpty();
case Operation::RegroupOutline:
{
PageItemModel::SelectionInfo info = m_model->getSelectionInfo(selection);
return info.isSingleDocument();
}
case Operation::RegroupAlternatingPages:
case Operation::RegroupAlternatingPagesReversed:
{
PageItemModel::SelectionInfo info = m_model->getSelectionInfo(selection);
return info.isTwoDocuments();
}
default:
Q_ASSERT(false);
break;
}
return false;
}
void MainWindow::performOperation(Operation operation)
{
switch (operation)
{
case Operation::Clear:
{
m_model->clear();
QPixmapCache::clear();
break;
}
case Operation::CloneSelection:
{
m_model->cloneSelection(ui->documentItemsView->selectionModel()->selection().indexes());
break;
}
case Operation::RemoveSelection:
{
m_model->removeSelection(ui->documentItemsView->selectionModel()->selection().indexes());
break;
}
case Operation::RestoreRemovedItems:
{
QModelIndexList restoredItemIndices = m_model->restoreRemovedItems();
QItemSelection itemSelection;
for (const QModelIndex& index : restoredItemIndices)
{
itemSelection.select(index, index);
}
ui->documentItemsView->selectionModel()->select(itemSelection, QItemSelectionModel::ClearAndSelect);
break;
}
case Operation::Undo:
{
m_model->undo();
break;
}
case Operation::Redo:
{
m_model->redo();
break;
}
case Operation::Cut:
case Operation::Copy:
{
QModelIndexList indices = ui->documentItemsView->selectionModel()->selection().indexes();
if (indices.isEmpty())
{
return;
}
if (QMimeData* mimeData = m_model->mimeData(indices))
{
QApplication::clipboard()->setMimeData(mimeData);
}
ui->documentItemsView->clearSelection();
m_dropAction = (operation == Operation::Cut) ? Qt::MoveAction : Qt::CopyAction;
break;
}
case Operation::Paste:
{
QModelIndexList indices = ui->documentItemsView->selectionModel()->selection().indexes();
int insertRow = m_model->rowCount(QModelIndex()) - 1;
if (!indices.isEmpty())
{
insertRow = indices.back().row();
}
QModelIndex insertIndex = m_model->index(insertRow, 0, QModelIndex());
const QMimeData* mimeData = QApplication::clipboard()->mimeData();
if (m_model->canDropMimeData(mimeData, m_dropAction, -1, -1, insertIndex))
{
m_model->dropMimeData(mimeData, m_dropAction, -1, -1, insertIndex);
}
break;
}
case Operation::Group:
m_model->group(ui->documentItemsView->selectionModel()->selection().indexes());
break;
case Operation::Ungroup:
m_model->ungroup(ui->documentItemsView->selectionModel()->selection().indexes());
break;
case Operation::SelectNone:
ui->documentItemsView->clearSelection();
break;
case Operation::SelectAll:
ui->documentItemsView->selectAll();
break;
case Operation::SelectEven:
ui->documentItemsView->selectionModel()->select(m_model->getSelectionEven(), QItemSelectionModel::ClearAndSelect);
break;
case Operation::SelectOdd:
ui->documentItemsView->selectionModel()->select(m_model->getSelectionOdd(), QItemSelectionModel::ClearAndSelect);
break;
case Operation::SelectPortrait:
ui->documentItemsView->selectionModel()->select(m_model->getSelectionPortrait(), QItemSelectionModel::ClearAndSelect);
break;
case Operation::SelectLandscape:
ui->documentItemsView->selectionModel()->select(m_model->getSelectionLandscape(), QItemSelectionModel::ClearAndSelect);
break;
case Operation::ZoomIn:
{
QSize pageImageSize = m_delegate->getPageImageSize();
pageImageSize *= 1.2;
pageImageSize = pageImageSize.boundedTo(getMaxPageImageSize());
if (pageImageSize != m_delegate->getPageImageSize())
{
m_delegate->setPageImageSize(pageImageSize);
}
break;
}
case Operation::ZoomOut:
{
QSize pageImageSize = m_delegate->getPageImageSize();
pageImageSize /= 1.2;
pageImageSize = pageImageSize.expandedTo(getMinPageImageSize());
if (pageImageSize != m_delegate->getPageImageSize())
{
m_delegate->setPageImageSize(pageImageSize);
}
break;
}
case Operation::RotateLeft:
m_model->rotateLeft(ui->documentItemsView->selectionModel()->selection().indexes());
break;
case Operation::RotateRight:
m_model->rotateRight(ui->documentItemsView->selectionModel()->selection().indexes());
break;
case Operation::GetSource:
QDesktopServices::openUrl(QUrl("https://github.com/JakubMelka/PDF4QT"));
break;
case Operation::BecomeSponsor:
QDesktopServices::openUrl(QUrl("https://github.com/sponsors/JakubMelka"));
break;
case Operation::InsertEmptyPage:
m_model->insertEmptyPage(ui->documentItemsView->selectionModel()->selection().indexes());
break;
case Operation::About:
{
PDFAboutDialog aboutDialog(this);
aboutDialog.exec();
break;
}
case Operation::PrepareIconTheme:
m_iconTheme.prepareTheme();
break;
case Operation::Unite:
case Operation::Separate:
case Operation::SeparateGrouped:
{
PageItemModel::AssembleMode assembleMode = PageItemModel::AssembleMode::Unite;
switch (operation)
{
case Operation::Unite:
assembleMode = PageItemModel::AssembleMode::Unite;
break;
case Operation::Separate:
assembleMode = PageItemModel::AssembleMode::Separate;
break;
case Operation::SeparateGrouped:
assembleMode = PageItemModel::AssembleMode::SeparateGrouped;
break;
default:
Q_ASSERT(false);
}
std::vector<std::vector<pdf::PDFDocumentManipulator::AssembledPage>> assembledDocuments = m_model->getAssembledPages(assembleMode);
// Check we have something to process
if (assembledDocuments.empty())
{
QMessageBox::critical(this, tr("Error"), tr("No documents to assemble."));
break;
}
AssembleOutputSettingsDialog dialog(m_settings.directory, this);
if (dialog.exec() == QDialog::Accepted)
{
pdf::PDFDocumentManipulator manipulator;
// Add documents and images
for (const auto& documentItem : m_model->getDocuments())
{
manipulator.addDocument(documentItem.first, &documentItem.second.document);
}
for (const auto& imageItem : m_model->getImages())
{
manipulator.addImage(imageItem.first, imageItem.second.image);
}
// Jakub Melka: create assembled documents
pdf::PDFOperationResult result(true);
std::vector<std::pair<QString, pdf::PDFDocument>> assembledDocumentStorage;
int sourceDocumentIndex = 1;
int assembledDocumentIndex = 1;
int sourcePageIndex = 1;
int documentCount = int(m_model->getDocuments().size());
QString directory = dialog.getDirectory();
QString fileNameTemplate = dialog.getFileName();
const bool isOverwriteEnabled = dialog.isOverwriteFiles();
pdf::PDFDocumentManipulator::OutlineMode outlineMode = dialog.getOutlineMode();
manipulator.setOutlineMode(outlineMode);
if (!directory.endsWith('/'))
{
directory += "/";
}
auto replaceInString = [](QString& templateString, QChar character, int number)
{
int index = templateString.indexOf(character, 0, Qt::CaseSensitive);
if (index != -1)
{
int lastIndex = templateString.lastIndexOf(character, -1, Qt::CaseSensitive);
int count = lastIndex - index + 1;
QString textNumber = QString::number(number);
textNumber = textNumber.rightJustified(count, '0', false);
templateString.remove(index, count);
templateString.insert(index, textNumber);
}
};
for (const std::vector<pdf::PDFDocumentManipulator::AssembledPage>& assembledPages : assembledDocuments)
{
pdf::PDFOperationResult currentResult = manipulator.assemble(assembledPages);
if (!currentResult && result)
{
result = currentResult;
break;
}
pdf::PDFDocumentManipulator::AssembledPage samplePage = assembledPages.front();
sourceDocumentIndex = samplePage.documentIndex == -1 ? documentCount + samplePage.imageIndex : samplePage.documentIndex;
sourcePageIndex = qMax(int(samplePage.pageIndex + 1), 1);
QString fileName = fileNameTemplate;
replaceInString(fileName, '#', assembledDocumentIndex);
replaceInString(fileName, '@', sourcePageIndex);
replaceInString(fileName, '%', sourceDocumentIndex);
if (!fileName.endsWith(".pdf"))
{
fileName += ".pdf";
}
fileName.prepend(directory);
assembledDocumentStorage.emplace_back(std::make_pair(std::move(fileName), manipulator.takeAssembledDocument()));
++assembledDocumentIndex;
}
if (!result)
{
QMessageBox::critical(this, tr("Error"), result.getErrorMessage());
return;
}
// Now, try to save files
for (const auto& assembledDocumentItem : assembledDocumentStorage)
{
QString filename = assembledDocumentItem.first;
const pdf::PDFDocument* document = &assembledDocumentItem.second;
const bool isDocumentFileAlreadyExisting = QFile::exists(filename);
if (!isOverwriteEnabled && isDocumentFileAlreadyExisting)
{
QMessageBox::critical(this, tr("Error"), tr("Document with given filename already exists."));
return;
}
pdf::PDFDocumentWriter writer(nullptr);
pdf::PDFOperationResult writeResult = writer.write(filename, document, isDocumentFileAlreadyExisting);
if (!writeResult)
{
QMessageBox::critical(this, tr("Error"), writeResult.getErrorMessage());
return;
}
}
}
break;
}
case Operation::InsertImage:
{
QStringList filters;
for (const QByteArray& imageFormat : QImageReader::supportedImageFormats())
{
filters << QString::fromLatin1(imageFormat).toLower();
}
QString filter = tr("Images (*.%1)").arg(filters.join(" *."));
QStringList fileNames = QFileDialog::getOpenFileNames(this, tr("Select Image(s)"), m_settings.directory, filter, nullptr);
if (!fileNames.isEmpty())
{
QModelIndexList indexes = ui->documentItemsView->selectionModel()->selection().indexes();
QModelIndex insertIndex = !indexes.isEmpty() ? indexes.front() : QModelIndex();
for (const QString& fileName : fileNames)
{
m_model->insertImage(fileName, insertIndex);
insertIndex = insertIndex.sibling(insertIndex.row() + 1, insertIndex.column());
}
}
break;
}
case Operation::InsertPDF:
{
QStringList fileNames = QFileDialog::getOpenFileNames(this, tr("Select PDF document(s)"), m_settings.directory, tr("PDF document (*.pdf)"));
if (!fileNames.isEmpty())
{
QModelIndexList indexes = ui->documentItemsView->selectionModel()->selection().indexes();
QModelIndex insertIndex = !indexes.isEmpty() ? indexes.front() : QModelIndex();
for (const QString& fileName : fileNames)
{
if (!insertDocument(fileName, insertIndex))
{
break;
}
insertIndex = insertIndex.sibling(insertIndex.row() + 1, insertIndex.column());
}
}
break;
}
case Operation::ReplaceSelection:
{
QModelIndexList indexes = ui->documentItemsView->selectionModel()->selection().indexes();
if (indexes.isEmpty())
{
// Jakub Melka: we have nothing to do, selection is empty
return;
}
QString fileName = QFileDialog::getOpenFileName(this, tr("Select PDF document"), m_settings.directory, tr("PDF document (*.pdf)"));
if (!fileName.isEmpty())
{
insertDocument(fileName, indexes.back());
m_model->removeSelection(indexes);
}
break;
}
case Operation::InvertSelection:
{
QModelIndex rootIndex = ui->documentItemsView->rootIndex();
if (rootIndex.isValid())
{
QModelIndex firstIndex = rootIndex.model()->index(0, 0, rootIndex);
QModelIndex lastIndex = rootIndex.model()->index(rootIndex.model()->rowCount() - 1, 0, rootIndex);
QItemSelection selection(firstIndex, lastIndex);
ui->documentItemsView->selectionModel()->select(selection, QItemSelectionModel::Toggle);
}
break;
}
case Operation::RegroupEvenOdd:
{
QModelIndexList indexes = ui->documentItemsView->selectionModel()->selection().indexes();
m_model->regroupEvenOdd(indexes);
break;
}
case Operation::RegroupPaired:
{
QModelIndexList indexes = ui->documentItemsView->selectionModel()->selection().indexes();
m_model->regroupPaired(indexes);
break;
}
case Operation::RegroupOutline:
{
QModelIndexList indexes = ui->documentItemsView->selectionModel()->selection().indexes();
if (!indexes.isEmpty())
{
PageItemModel::SelectionInfo selectionInfo = m_model->getSelectionInfo(indexes);
const std::map<int, DocumentItem>& documents = m_model->getDocuments();
auto it = documents.find(selectionInfo.firstDocumentIndex);
if (it != documents.end())
{
const pdf::PDFDocument* document = &it->second.document;
SelectOutlineToRegroupDialog dialog(document, this);
if (dialog.exec() == SelectOutlineToRegroupDialog::Accepted)
{
std::vector<pdf::PDFInteger> breakPageIndices;
std::vector<const pdf::PDFOutlineItem*> outlineItems = dialog.getSelectedOutlineItems();
// Jakub Melka: Resolve outline items. Try to find an index
// of page of each outline item.
for (const pdf::PDFOutlineItem* item : outlineItems)
{
const pdf::PDFAction* action = item->getAction();
const pdf::PDFActionGoTo* actionGoto = dynamic_cast<const pdf::PDFActionGoTo*>(action);
if (actionGoto)
{
pdf::PDFDestination destination = actionGoto->getDestination();
if (destination.getDestinationType() == pdf::DestinationType::Named)
{
if (const pdf::PDFDestination* targetDestination = document->getCatalog()->getNamedDestination(destination.getName()))
{
destination = *targetDestination;
}
else
{
destination = pdf::PDFDestination();
}
}
if (destination.isValid())
{
const size_t pageIndex = document->getCatalog()->getPageIndexFromPageReference(destination.getPageReference());
if (pageIndex != pdf::PDFCatalog::INVALID_PAGE_INDEX)
{
breakPageIndices.push_back(pageIndex + 1);
}
}
}
}
std::sort(breakPageIndices.begin(), breakPageIndices.end());
breakPageIndices.erase(std::unique(breakPageIndices.begin(), breakPageIndices.end()), breakPageIndices.end());
m_model->regroupOutline(indexes, breakPageIndices);
}
}
}
break;
}
case Operation::RegroupAlternatingPages:
{
QModelIndexList indexes = ui->documentItemsView->selectionModel()->selection().indexes();
m_model->regroupAlternatingPages(indexes, false);
break;
}
case Operation::RegroupAlternatingPagesReversed:
{
QModelIndexList indexes = ui->documentItemsView->selectionModel()->selection().indexes();
m_model->regroupAlternatingPages(indexes, true);
break;
}
default:
{
Q_ASSERT(false);
break;
}
}
updateActions();
}
void MainWindow::dragEnterEvent(QDragEnterEvent* event)
{
const QMimeData* mimeData = event->mimeData();
if (mimeData->hasImage() || mimeData->hasUrls())
{
event->acceptProposedAction();
}
}
void MainWindow::dropEvent(QDropEvent* event)
{
event->ignore();
const QMimeData* mimeData = event->mimeData();
if (mimeData->hasImage())
{
QImage image = mimeData->imageData().value<QImage>();
if (!image.isNull())
{
QModelIndexList indexes = ui->documentItemsView->selectionModel()->selection().indexes();
QModelIndex insertIndex = !indexes.isEmpty() ? indexes.front() : QModelIndex();
m_model->insertImage(image, insertIndex);
event->accept();
}
}
else if (mimeData->hasUrls())
{
QModelIndexList indexes = ui->documentItemsView->selectionModel()->selection().indexes();
QModelIndex insertIndex = !indexes.isEmpty() ? indexes.front() : QModelIndex();
QList<QUrl> urls = mimeData->urls();
std::reverse(urls.begin(), urls.end());
QList<QByteArray> supportedImageFormats = QImageReader::supportedImageFormats();
for (QUrl url : urls)
{
event->accept();
if (url.isLocalFile())
{
QString fileName = url.toLocalFile();
QFileInfo fileInfo(fileName);
QString suffix = fileInfo.suffix().toLower();
if (supportedImageFormats.contains(suffix.toUtf8()))
{
if (m_model->insertImage(fileName, insertIndex) == -1)
{
// Exit the loop if the image was not inserted.
event->ignore();
break;
}
}
else if (suffix == "pdf")
{
if (!insertDocument(url.toLocalFile(), insertIndex))
{
// Exit the loop if the document was not inserted. This could be because
// the document requires a password and the user might have provided an incorrect one,
// or the file couldn't be opened. We don't want to proceed with the next file.
event->ignore();
break;
}
}
else
{
// We ignore other file extensions.
}
}
}
}
}
} // namespace pdfpagemaster