DocPage Organizer: Group by bookmarks

This commit is contained in:
Jakub Melka 2021-08-04 16:43:26 +02:00
parent 7a40427d10
commit 0ba06062e5
11 changed files with 590 additions and 7 deletions

View File

@ -46,19 +46,22 @@ SOURCES += \
main.cpp \
mainwindow.cpp \
pageitemdelegate.cpp \
pageitemmodel.cpp
pageitemmodel.cpp \
selectbookmarkstoregroupdialog.cpp
FORMS += \
aboutdialog.ui \
assembleoutputsettingsdialog.ui \
mainwindow.ui
mainwindow.ui \
selectbookmarkstoregroupdialog.ui
HEADERS += \
aboutdialog.h \
assembleoutputsettingsdialog.h \
mainwindow.h \
pageitemdelegate.h \
pageitemmodel.h
pageitemmodel.h \
selectbookmarkstoregroupdialog.h
RESOURCES += \
resources.qrc

View File

@ -20,7 +20,9 @@
#include "aboutdialog.h"
#include "assembleoutputsettingsdialog.h"
#include "selectbookmarkstoregroupdialog.h"
#include "pdfaction.h"
#include "pdfwidgetutils.h"
#include "pdfdocumentreader.h"
#include "pdfdocumentwriter.h"
@ -780,7 +782,64 @@ void MainWindow::performOperation(Operation operation)
case Operation::RegroupBookmarks:
{
QModelIndexList indexes = ui->documentItemsView->selectionModel()->selection().indexes();
m_model->regroupBookmarks(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;
SelectBookmarksToRegroupDialog dialog(document, this);
if (dialog.exec() == SelectBookmarksToRegroupDialog::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->regroupBookmarks(indexes, breakPageIndices);
}
}
}
break;
}

View File

@ -567,6 +567,7 @@ PageItemModel::SelectionInfo PageItemModel::getSelectionInfo(const QModelIndexLi
info.documentCount = int(documents.size());
info.imageCount = int(images.size());
info.firstDocumentIndex = !documents.empty() ? *documents.begin() : 0;
return info;
}
@ -646,11 +647,49 @@ void PageItemModel::regroupPaired(const QModelIndexList& list)
}
}
void PageItemModel::regroupBookmarks(const QModelIndexList& list)
void PageItemModel::regroupBookmarks(const QModelIndexList& list, const std::vector<pdf::PDFInteger>& indices)
{
Q_ASSERT(false);
if (list.empty())
{
return;
}
Modifier modifier(this);
std::vector<PageGroupItem> pageGroupItems = m_pageGroupItems;
std::vector<PageGroupItem::GroupItem> extractedItems = extractItems(pageGroupItems, list);
std::sort(extractedItems.begin(), extractedItems.end(), [](const auto& l, const auto& r) { return l.pageIndex < r.pageIndex; });
PageGroupItem item;
for (auto it = extractedItems.begin(); it != extractedItems.cend(); ++it)
{
PageGroupItem::GroupItem groupItem = *it;
if (std::binary_search(indices.cbegin(), indices.cend(), groupItem.pageIndex) &&
!item.groups.empty())
{
updateItemCaptionAndTags(item);
pageGroupItems.emplace_back(std::move(item));
item = PageGroupItem();
}
item.groups.push_back(groupItem);
}
if (!item.groups.empty())
{
updateItemCaptionAndTags(item);
pageGroupItems.emplace_back(std::move(item));
item = PageGroupItem();
}
if (pageGroupItems != m_pageGroupItems)
{
beginResetModel();
m_pageGroupItems = std::move(pageGroupItems);
endResetModel();
}
}
void PageItemModel::regroupAlternatingPages(const QModelIndexList& list, bool reversed)

View File

@ -173,6 +173,7 @@ public:
int documentCount = 0;
int imageCount = 0;
int blankPageCount = 0;
int firstDocumentIndex = 0;
bool isDocumentOnly() const { return documentCount > 0 && imageCount == 0 && blankPageCount == 0; }
bool isSingleDocument() const { return isDocumentOnly() && documentCount == 1; }
@ -183,7 +184,7 @@ public:
void regroupEvenOdd(const QModelIndexList& list);
void regroupPaired(const QModelIndexList& list);
void regroupBookmarks(const QModelIndexList& list);
void regroupBookmarks(const QModelIndexList& list, const std::vector<pdf::PDFInteger>& indices);
void regroupAlternatingPages(const QModelIndexList& list, bool reversed);
bool canUndo() const { return !m_undoSteps.empty(); }

View File

@ -38,5 +38,6 @@
<file>resources/regroup-pairs.svg</file>
<file>resources/undo.svg</file>
<file>resources/redo.svg</file>
<file>resources/bookmark.svg</file>
</qresource>
</RCC>

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="30mm"
height="30mm"
viewBox="0 0 30 30"
version="1.1"
id="svg5291"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="bookmark.svg">
<defs
id="defs5285" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35"
inkscape:cx="-278.8134"
inkscape:cy="145.70776"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="3840"
inkscape:window-height="2035"
inkscape:window-x="-13"
inkscape:window-y="-13"
inkscape:window-maximized="1" />
<metadata
id="metadata5288">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
<dc:creator>
<cc:Agent>
<dc:title>Jakub Melka</dc:title>
</cc:Agent>
</dc:creator>
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" />
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#ShareAlike" />
</cc:License>
</rdf:RDF>
</metadata>
<g
inkscape:label="Vrstva 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-267)">
<path
style="fill:#000000;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:0.1545139;stroke-miterlimit:4;stroke-dasharray:none"
d="m 5.8113839,294.11793 v -0.3071 l 0.07087,-23.95425 18.8279381,-0.11811 0.09449,24.68657 -9.307663,-10.08724 z"
id="path831"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,161 @@
// 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 "selectbookmarkstoregroupdialog.h"
#include "ui_selectbookmarkstoregroupdialog.h"
#include "pdfitemmodels.h"
#include "pdfwidgetutils.h"
#include <QMenu>
namespace pdfdocpage
{
SelectBookmarksToRegroupDialog::SelectBookmarksToRegroupDialog(const pdf::PDFDocument* document, QWidget* parent) :
QDialog(parent),
ui(new Ui::SelectBookmarksToRegroupDialog),
m_document(document),
m_model(nullptr)
{
ui->setupUi(this);
QIcon bookmarkIcon(":/pdfdocpage/resources/bookmark.svg");
m_model = new pdf::PDFSelectableOutlineTreeItemModel(qMove(bookmarkIcon), this);
ui->bookmarksView->setModel(m_model);
ui->bookmarksView->header()->hide();
m_model->setDocument(pdf::PDFModifiedDocument(const_cast<pdf::PDFDocument*>(document), nullptr));
ui->bookmarksView->expandToDepth(2);
ui->bookmarksView->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui->bookmarksView, &QTreeView::customContextMenuRequested, this, &SelectBookmarksToRegroupDialog::onViewContextMenuRequested);
QSize size = pdf::PDFWidgetUtils::scaleDPI(this, QSize(400, 600));
setMinimumSize(size);
}
SelectBookmarksToRegroupDialog::~SelectBookmarksToRegroupDialog()
{
delete ui;
}
std::vector<const pdf::PDFOutlineItem*> SelectBookmarksToRegroupDialog::getSelectedOutlineItems() const
{
return m_model->getSelectedItems();
}
void SelectBookmarksToRegroupDialog::onViewContextMenuRequested(const QPoint& pos)
{
QMenu menu;
menu.addAction(tr("Select All"), this, &SelectBookmarksToRegroupDialog::selectAll);
menu.addAction(tr("Deselect All"), this, &SelectBookmarksToRegroupDialog::deselectAll);
menu.addAction(tr("Invert Selection"), this, &SelectBookmarksToRegroupDialog::invertSelection);
menu.addSeparator();
menu.addAction(tr("Select Level 1"), this, &SelectBookmarksToRegroupDialog::selectLevel1);
menu.addAction(tr("Select Level 2"), this, &SelectBookmarksToRegroupDialog::selectLevel2);
QModelIndex index = ui->bookmarksView->indexAt(pos);
if (index.isValid())
{
m_menuIndex = index;
menu.addSeparator();
menu.addAction(tr("Select subtree"), this, &SelectBookmarksToRegroupDialog::selectSubtree);
menu.addAction(tr("Deselect subtree"), this, &SelectBookmarksToRegroupDialog::deselectSubtree);
}
menu.exec(ui->bookmarksView->mapToGlobal(pos));
}
void SelectBookmarksToRegroupDialog::manipulateTree(const QModelIndex& index,
const std::function<void (QModelIndex)>& manipulator)
{
if (index.isValid())
{
manipulator(index);
}
const int count = m_model->rowCount(index);
for (int i = 0; i < count; ++i)
{
QModelIndex childIndex = m_model->index(i, 0, index);
manipulateTree(childIndex, manipulator);
}
}
std::function<void (QModelIndex)> SelectBookmarksToRegroupDialog::createCheckByDepthManipulator(int targetDepth) const
{
auto manipulator = [this, targetDepth](QModelIndex index)
{
int depth = 1;
QModelIndex parentIndex = index.parent();
while (parentIndex.isValid())
{
++depth;
parentIndex = parentIndex.parent();
}
if (depth == targetDepth)
{
m_model->setData(index, Qt::Checked, Qt::CheckStateRole);
}
};
return manipulator;
}
void SelectBookmarksToRegroupDialog::selectAll()
{
manipulateTree(ui->bookmarksView->rootIndex(), [this](QModelIndex index) { m_model->setData(index, Qt::Checked, Qt::CheckStateRole); });
}
void SelectBookmarksToRegroupDialog::deselectAll()
{
manipulateTree(ui->bookmarksView->rootIndex(), [this](QModelIndex index) { m_model->setData(index, Qt::Unchecked, Qt::CheckStateRole); });
}
void SelectBookmarksToRegroupDialog::invertSelection()
{
auto manipulator = [this](QModelIndex index)
{
const bool isChecked = index.data(Qt::CheckStateRole).toInt() == Qt::Checked;
m_model->setData(index, isChecked ? Qt::Unchecked : Qt::Checked, Qt::CheckStateRole);
};
manipulateTree(ui->bookmarksView->rootIndex(), manipulator);
}
void SelectBookmarksToRegroupDialog::selectLevel1()
{
manipulateTree(ui->bookmarksView->rootIndex(), createCheckByDepthManipulator(1));
}
void SelectBookmarksToRegroupDialog::selectLevel2()
{
manipulateTree(ui->bookmarksView->rootIndex(), createCheckByDepthManipulator(2));
}
void SelectBookmarksToRegroupDialog::selectSubtree()
{
manipulateTree(m_menuIndex, [this](QModelIndex index) { m_model->setData(index, Qt::Checked, Qt::CheckStateRole); });
}
void SelectBookmarksToRegroupDialog::deselectSubtree()
{
manipulateTree(m_menuIndex, [this](QModelIndex index) { m_model->setData(index, Qt::Unchecked, Qt::CheckStateRole); });
}
} // namespace pdfdocpage

View File

@ -0,0 +1,73 @@
// 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/>.
#ifndef PDFDOCPAGEORGANIZER_SELECTBOOKMARKSTOREGROUPDIALOG_H
#define PDFDOCPAGEORGANIZER_SELECTBOOKMARKSTOREGROUPDIALOG_H
#include "pdfdocument.h"
#include <QDialog>
namespace Ui
{
class SelectBookmarksToRegroupDialog;
}
namespace pdf
{
class PDFSelectableOutlineTreeItemModel;
}
namespace pdfdocpage
{
class SelectBookmarksToRegroupDialog : public QDialog
{
Q_OBJECT
public:
explicit SelectBookmarksToRegroupDialog(const pdf::PDFDocument* document, QWidget* parent);
virtual ~SelectBookmarksToRegroupDialog() override;
std::vector<const pdf::PDFOutlineItem*> getSelectedOutlineItems() const;
private:
void selectAll();
void deselectAll();
void invertSelection();
void selectLevel1();
void selectLevel2();
void selectSubtree();
void deselectSubtree();
void onViewContextMenuRequested(const QPoint& pos);
void manipulateTree(const QModelIndex& index, const std::function<void (QModelIndex)>& manipulator);
std::function<void (QModelIndex)> createCheckByDepthManipulator(int targetDepth) const;
Ui::SelectBookmarksToRegroupDialog* ui;
const pdf::PDFDocument* m_document;
pdf::PDFSelectableOutlineTreeItemModel* m_model;
QModelIndex m_menuIndex;
};
} // namespace pdfdocpage
#endif // PDFDOCPAGEORGANIZER_SELECTBOOKMARKSTOREGROUPDIALOG_H

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SelectBookmarksToRegroupDialog</class>
<widget class="QDialog" name="SelectBookmarksToRegroupDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>516</width>
<height>494</height>
</rect>
</property>
<property name="windowTitle">
<string>Select Bookmarks</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTreeView" name="bookmarksView"/>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>SelectBookmarksToRegroupDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>SelectBookmarksToRegroupDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -811,4 +811,72 @@ PDFAttachmentsTreeItem::~PDFAttachmentsTreeItem()
}
QVariant PDFSelectableOutlineTreeItemModel::data(const QModelIndex& index, int role) const
{
if (role == Qt::CheckStateRole)
{
if (!index.isValid())
{
return QVariant();
}
const PDFOutlineTreeItem* item = static_cast<const PDFOutlineTreeItem*>(index.internalPointer());
const PDFOutlineItem* outlineItem = item->getOutlineItem();
const bool isChecked = m_selectedItems.count(outlineItem);
return isChecked ? Qt::Checked : Qt::Unchecked;
}
return BaseClass::data(index, role);
}
void PDFSelectableOutlineTreeItemModel::update()
{
BaseClass::update();
m_selectedItems.clear();
}
Qt::ItemFlags PDFSelectableOutlineTreeItemModel::flags(const QModelIndex& index) const
{
Qt::ItemFlags flags = BaseClass::flags(index);
if (index.isValid())
{
flags |= Qt::ItemIsUserCheckable;
}
return flags;
}
bool PDFSelectableOutlineTreeItemModel::setData(const QModelIndex& index, const QVariant& value, int role)
{
if (role != Qt::CheckStateRole || !index.isValid())
{
return false;
}
const PDFOutlineTreeItem* item = static_cast<const PDFOutlineTreeItem*>(index.internalPointer());
const PDFOutlineItem* outlineItem = item->getOutlineItem();
if (outlineItem)
{
if (value.toInt() == Qt::Checked)
{
m_selectedItems.insert(outlineItem);
}
else
{
m_selectedItems.erase(outlineItem);
}
return true;
}
return false;
}
std::vector<const PDFOutlineItem*> PDFSelectableOutlineTreeItemModel::getSelectedItems() const
{
return std::vector<const PDFOutlineItem*>(m_selectedItems.cbegin(), m_selectedItems.cend());
}
} // namespace pdf

View File

@ -25,6 +25,8 @@
#include <QPixmapCache>
#include <QAbstractItemModel>
#include <set>
namespace pdf
{
class PDFAction;
@ -173,6 +175,31 @@ private:
QIcon m_icon;
};
class Pdf4QtLIBSHARED_EXPORT PDFSelectableOutlineTreeItemModel : public PDFOutlineTreeItemModel
{
Q_OBJECT
private:
using BaseClass = PDFOutlineTreeItemModel;
public:
PDFSelectableOutlineTreeItemModel(QIcon icon, QObject* parent) :
BaseClass(qMove(icon), parent)
{
}
virtual QVariant data(const QModelIndex& index, int role) const override;
virtual void update() override;
virtual Qt::ItemFlags flags(const QModelIndex& index) const override;
virtual bool setData(const QModelIndex& index, const QVariant& value, int role) override;
std::vector<const PDFOutlineItem*> getSelectedItems() const;
private:
std::set<const PDFOutlineItem*> m_selectedItems;
};
class PDFAttachmentsTreeItem : public PDFTreeItem
{
public: