From be0d02bb5ca06c0ce8145a2bcd83f664b41f8b7e Mon Sep 17 00:00:00 2001 From: Jakub Melka Date: Sun, 1 Oct 2023 17:35:42 +0200 Subject: [PATCH] Issue #76: Basic functionality of outline editing --- Pdf4QtLib/sources/pdfaction.cpp | 170 ++++++++++++++ Pdf4QtLib/sources/pdfaction.h | 72 +++++- Pdf4QtLib/sources/pdfitemmodels.cpp | 275 ++++++++++++++++++++++- Pdf4QtLib/sources/pdfitemmodels.h | 47 +++- Pdf4QtLib/sources/pdfoutline.cpp | 23 ++ Pdf4QtLib/sources/pdfoutline.h | 2 + Pdf4QtViewer/pdfsidebarwidget.cpp | 129 ++++++++++- Pdf4QtViewer/pdfsidebarwidget.h | 2 + Pdf4QtViewer/pdfviewermainwindow.cpp | 2 +- Pdf4QtViewer/pdfviewermainwindowlite.cpp | 2 +- 10 files changed, 711 insertions(+), 13 deletions(-) diff --git a/Pdf4QtLib/sources/pdfaction.cpp b/Pdf4QtLib/sources/pdfaction.cpp index 116856c..3be3982 100644 --- a/Pdf4QtLib/sources/pdfaction.cpp +++ b/Pdf4QtLib/sources/pdfaction.cpp @@ -370,6 +370,17 @@ void PDFAction::fillActionList(std::vector& actionList) const } } +void PDFAction::cloneActionList(const PDFAction* sourceAction) +{ + if (sourceAction) + { + for (const auto& action : sourceAction->m_nextActions) + { + m_nextActions.push_back(action->clone()); + } + } +} + PDFDestination PDFDestination::parse(const PDFObjectStorage* storage, PDFObject object) { PDFDestination result; @@ -711,6 +722,16 @@ QString PDFActionURI::getURIString() const return QString::fromUtf8(m_URI); } +void PDFActionURI::setURI(const QByteArray& newURI) +{ + m_URI = newURI; +} + +void PDFActionURI::setIsMap(bool newIsMap) +{ + m_isMap = newIsMap; +} + void PDFActionGoTo::setDestination(const PDFDestination& destination) { m_destination = destination; @@ -721,4 +742,153 @@ void PDFActionGoTo::setStructureDestination(const PDFDestination& structureDesti m_structureDestination = structureDestination; } +PDFActionPtr PDFActionGoTo::clone() const +{ + PDFActionGoTo* clonedAction = new PDFActionGoTo(getDestination(), getStructureDestination()); + clonedAction->cloneActionList(this); + return PDFActionPtr(clonedAction); +} + +PDFActionPtr PDFActionGoToR::clone() const +{ + PDFActionGoToR* clonedAction = new PDFActionGoToR(getDestination(), + getStructureDestination(), + getFileSpecification(), + isNewWindow()); + clonedAction->cloneActionList(this); + return PDFActionPtr(clonedAction); +} + +PDFActionPtr PDFActionGoToE::clone() const +{ + PDFActionGoToE* clonedAction = new PDFActionGoToE(getDestination(), + getFileSpecification(), + isNewWindow(), + getTarget()); + clonedAction->cloneActionList(this); + return PDFActionPtr(clonedAction); +} + +PDFActionPtr PDFActionGoToDp::clone() const +{ + PDFActionGoToDp* clonedAction = new PDFActionGoToDp(getDocumentPart()); + clonedAction->cloneActionList(this); + return PDFActionPtr(clonedAction); +} + +PDFActionPtr PDFActionLaunch::clone() const +{ + PDFActionLaunch* clonedAction = new PDFActionLaunch(getFileSpecification(), isNewWindow(), getWinSpecification()); + clonedAction->cloneActionList(this); + return PDFActionPtr(clonedAction); +} + +PDFActionPtr PDFActionThread::clone() const +{ + PDFActionThread* clonedAction = new PDFActionThread(getFileSpecification(), getThread(), getBead()); + clonedAction->cloneActionList(this); + return PDFActionPtr(clonedAction); +} + +PDFActionPtr PDFActionURI::clone() const +{ + PDFActionURI* clonedAction = new PDFActionURI(getURI(), isMap()); + clonedAction->cloneActionList(this); + return PDFActionPtr(clonedAction); +} + +PDFActionPtr PDFActionSound::clone() const +{ + PDFActionSound* clonedAction = new PDFActionSound(*getSound(), getVolume(), isSynchronous(), isRepeat(), isMix()); + clonedAction->cloneActionList(this); + return PDFActionPtr(clonedAction); +} + +PDFActionPtr PDFActionMovie::clone() const +{ + PDFActionMovie* clonedAction = new PDFActionMovie(getAnnotation(), getTitle(), getOperation()); + clonedAction->cloneActionList(this); + return PDFActionPtr(clonedAction); +} + +PDFActionPtr PDFActionHide::clone() const +{ + PDFActionHide* clonedAction = new PDFActionHide(getAnnotations(), getFieldNames(), isHide()); + clonedAction->cloneActionList(this); + return PDFActionPtr(clonedAction); +} + +PDFActionPtr PDFActionNamed::clone() const +{ + PDFActionNamed* clonedAction = new PDFActionNamed(getNamedActionType(), getCustomNamedAction()); + clonedAction->cloneActionList(this); + return PDFActionPtr(clonedAction); +} + +PDFActionPtr PDFActionSetOCGState::clone() const +{ + PDFActionSetOCGState* clonedAction = new PDFActionSetOCGState(getStateChangeItems(), isRadioButtonsPreserved()); + clonedAction->cloneActionList(this); + return PDFActionPtr(clonedAction); +} + +PDFActionPtr PDFActionTransition::clone() const +{ + PDFActionTransition* clonedAction = new PDFActionTransition(getTransition()); + clonedAction->cloneActionList(this); + return PDFActionPtr(clonedAction); +} + +PDFActionPtr PDFActionGoTo3DView::clone() const +{ + PDFActionGoTo3DView* clonedAction = new PDFActionGoTo3DView(getAnnotation(), getView()); + clonedAction->cloneActionList(this); + return PDFActionPtr(clonedAction); +} + +PDFActionPtr PDFActionJavaScript::clone() const +{ + PDFActionJavaScript* clonedAction = new PDFActionJavaScript(getJavaScript()); + clonedAction->cloneActionList(this); + return PDFActionPtr(clonedAction); +} + +PDFActionPtr PDFActionRichMediaExecute::clone() const +{ + PDFActionRichMediaExecute* clonedAction = new PDFActionRichMediaExecute(getRichMediaAnnotation(), + getRichMediaInstance(), + getCommand(), + getArguments()); + clonedAction->cloneActionList(this); + return PDFActionPtr(clonedAction); +} + +PDFActionPtr PDFActionSubmitForm::clone() const +{ + PDFActionSubmitForm* clonedAction = new PDFActionSubmitForm(getFieldScope(), getFieldList(), getUrl(), getCharset(), getFlags()); + clonedAction->cloneActionList(this); + return PDFActionPtr(clonedAction); +} + +PDFActionPtr PDFActionResetForm::clone() const +{ + PDFActionResetForm* clonedAction = new PDFActionResetForm(getFieldScope(), getFieldList(), getFlags()); + clonedAction->cloneActionList(this); + return PDFActionPtr(clonedAction); +} + +PDFActionPtr PDFActionImportDataForm::clone() const +{ + PDFActionImportDataForm* clonedAction = new PDFActionImportDataForm(getFile()); + clonedAction->cloneActionList(this); + return PDFActionPtr(clonedAction); +} + +PDFActionPtr PDFActionRendition::clone() const +{ + PDFActionRendition* clonedAction = new PDFActionRendition(m_rendition, getAnnotation(), getOperation(), getJavaScript()); + clonedAction->cloneActionList(this); + return PDFActionPtr(clonedAction); +} + } // namespace pdf diff --git a/Pdf4QtLib/sources/pdfaction.h b/Pdf4QtLib/sources/pdfaction.h index 88faaaf..9cf20cf 100644 --- a/Pdf4QtLib/sources/pdfaction.h +++ b/Pdf4QtLib/sources/pdfaction.h @@ -169,6 +169,12 @@ public: /// Returns list of actions to be executed std::vector getActionList() const; + /// Clone action + virtual PDFActionPtr clone() const = 0; + +protected: + void cloneActionList(const PDFAction* sourceAction); + private: static PDFActionPtr parseImpl(const PDFObjectStorage* storage, PDFObject object, std::set& usedReferences); @@ -180,7 +186,7 @@ private: /// Regular go-to action. Can contain also structure destinations, both regular page destination /// and structure destination are present, because if structure destination fails, then /// page destination can be used as fallback resolution. -class PDFActionGoTo : public PDFAction +class PDF4QTLIBSHARED_EXPORT PDFActionGoTo : public PDFAction { public: explicit inline PDFActionGoTo(PDFDestination destination, PDFDestination structureDestination) : @@ -194,6 +200,8 @@ public: void setDestination(const PDFDestination& destination); void setStructureDestination(const PDFDestination& structureDestination); + virtual PDFActionPtr clone() const override; + private: PDFDestination m_destination; PDFDestination m_structureDestination; @@ -218,6 +226,8 @@ public: const PDFFileSpecification& getFileSpecification() const { return m_fileSpecification; } bool isNewWindow() const { return m_newWindow; } + virtual PDFActionPtr clone() const override; + private: PDFDestination m_destination; PDFDestination m_structureDestination; @@ -244,6 +254,8 @@ public: bool isNewWindow() const { return m_newWindow; } const PDFObject& getTarget() const { return m_target; } + virtual PDFActionPtr clone() const override; + private: PDFDestination m_destination; PDFFileSpecification m_fileSpecification; @@ -262,6 +274,8 @@ public: PDFObjectReference getDocumentPart() const { return m_documentPart; } + virtual PDFActionPtr clone() const override; + private: PDFObjectReference m_documentPart; }; @@ -293,6 +307,8 @@ public: const Win& getWinSpecification() const { return m_win; } bool isNewWindow() const { return m_newWindow; } + virtual PDFActionPtr clone() const override; + private: PDFFileSpecification m_fileSpecification; bool m_newWindow = false; @@ -319,6 +335,8 @@ public: const Thread& getThread() const { return m_thread; } const Bead& getBead() const { return m_bead; } + virtual PDFActionPtr clone() const override; + private: PDFFileSpecification m_fileSpecification; Thread m_thread; @@ -344,6 +362,11 @@ public: /// to PDF specification, URI is UTF-8 encoded string. QString getURIString() const; + virtual PDFActionPtr clone() const override; + + void setURI(const QByteArray& newURI); + void setIsMap(bool newIsMap); + private: QByteArray m_URI; bool m_isMap; @@ -370,6 +393,8 @@ public: bool isRepeat() const { return m_isRepeat; } bool isMix() const { return m_isMix; } + virtual PDFActionPtr clone() const override; + private: PDFSound m_sound; PDFReal m_volume; @@ -403,6 +428,8 @@ public: const QString& getTitle() const { return m_title; } Operation getOperation() const { return m_operation; } + virtual PDFActionPtr clone() const override; + private: PDFObjectReference m_annotation; QString m_title; @@ -420,12 +447,22 @@ public: } + explicit inline PDFActionHide(const std::vector& annotations, const std::vector& fieldNames, bool hide) : + m_annotations(annotations), + m_fieldNames(fieldNames), + m_hide(hide) + { + + } + virtual ActionType getType() const override { return ActionType::Hide; } const std::vector& getAnnotations() const { return m_annotations; } const std::vector& getFieldNames() const { return m_fieldNames; } bool isHide() const { return m_hide; } + virtual PDFActionPtr clone() const override; + private: std::vector m_annotations; std::vector m_fieldNames; @@ -451,11 +488,20 @@ public: } + explicit inline PDFActionNamed(NamedActionType namedActionType, const QByteArray& customNamedAction) : + m_namedActionType(namedActionType), + m_customNamedAction(customNamedAction) + { + + } + virtual ActionType getType() const override { return ActionType::Named; } NamedActionType getNamedActionType() const { return m_namedActionType; } const QByteArray& getCustomNamedAction() const { return m_customNamedAction; } + virtual PDFActionPtr clone() const override; + private: NamedActionType m_namedActionType; QByteArray m_customNamedAction; @@ -475,7 +521,7 @@ public: using StateChangeItem = std::pair; using StateChangeItems = std::vector; - explicit inline PDFActionSetOCGState(StateChangeItems&& stateChangeItems, bool isRadioButtonsPreserved) : + explicit inline PDFActionSetOCGState(StateChangeItems stateChangeItems, bool isRadioButtonsPreserved) : m_items(qMove(stateChangeItems)), m_isRadioButtonsPreserved(isRadioButtonsPreserved) { @@ -487,6 +533,8 @@ public: const StateChangeItems& getStateChangeItems() const { return m_items; } bool isRadioButtonsPreserved() const { return m_isRadioButtonsPreserved; } + virtual PDFActionPtr clone() const override; + private: StateChangeItems m_items; bool m_isRadioButtonsPreserved; @@ -505,7 +553,7 @@ public: Play = 4 }; - explicit inline PDFActionRendition(std::optional&& rendition, PDFObjectReference annotation, Operation operation, QString javascript) : + explicit inline PDFActionRendition(std::optional rendition, PDFObjectReference annotation, Operation operation, QString javascript) : m_rendition(qMove(rendition)), m_annotation(annotation), m_operation(operation), @@ -521,6 +569,8 @@ public: Operation getOperation() const { return m_operation; } const QString& getJavaScript() const { return m_javascript; } + virtual PDFActionPtr clone() const override; + private: std::optional m_rendition; PDFObjectReference m_annotation; @@ -531,7 +581,7 @@ private: class PDFActionTransition : public PDFAction { public: - explicit inline PDFActionTransition(PDFPageTransition&& transition) : + explicit inline PDFActionTransition(PDFPageTransition transition) : m_transition(qMove(transition)) { @@ -541,6 +591,8 @@ public: const PDFPageTransition& getTransition() const { return m_transition; } + virtual PDFActionPtr clone() const override; + private: PDFPageTransition m_transition; }; @@ -560,6 +612,8 @@ public: const PDFObject& getAnnotation() const { return m_annotation; } const PDFObject& getView() const { return m_view; } + virtual PDFActionPtr clone() const override; + private: PDFObject m_annotation; PDFObject m_view; @@ -578,6 +632,8 @@ public: const QString& getJavaScript() const { return m_javaScript; } + virtual PDFActionPtr clone() const override; + private: QString m_javaScript; }; @@ -604,6 +660,8 @@ public: QString getCommand() const { return m_command; } PDFObject getArguments() const { return m_arguments; } + virtual PDFActionPtr clone() const override; + private: PDFObjectReference m_richMediaAnnotation; PDFObjectReference m_richMediaInstance; @@ -689,6 +747,8 @@ public: const QByteArray& getCharset() const { return m_charset; } SubmitFlags getFlags() const { return m_flags; } + virtual PDFActionPtr clone() const override; + private: PDFFileSpecification m_url; QByteArray m_charset; @@ -717,6 +777,8 @@ public: ResetFlags getFlags() const { return m_flags; } + virtual PDFActionPtr clone() const override; + private: ResetFlags m_flags = None; }; @@ -735,6 +797,8 @@ public: const PDFFileSpecification& getFile() const { return m_file; } + virtual PDFActionPtr clone() const override; + private: PDFFileSpecification m_file; }; diff --git a/Pdf4QtLib/sources/pdfitemmodels.cpp b/Pdf4QtLib/sources/pdfitemmodels.cpp index cfb347c..f205277 100644 --- a/Pdf4QtLib/sources/pdfitemmodels.cpp +++ b/Pdf4QtLib/sources/pdfitemmodels.cpp @@ -35,6 +35,13 @@ PDFTreeItem::~PDFTreeItem() qDeleteAll(m_children); } +PDFTreeItem* PDFTreeItem::takeChild(int index) +{ + PDFTreeItem* item = m_children.at(index); + m_children.erase(m_children.begin() + index); + return item; +} + PDFTreeItemModel::PDFTreeItemModel(QObject* parent) : QAbstractItemModel(parent), m_document(nullptr) @@ -368,6 +375,7 @@ QVariant PDFOutlineTreeItemModel::data(const QModelIndex& index, int role) const switch (role) { case Qt::DisplayRole: + case Qt::EditRole: return outlineItem->getTitle(); case Qt::ForegroundRole: @@ -409,6 +417,11 @@ void PDFOutlineTreeItemModel::update() } if (outlineRoot) { + if (m_editable) + { + outlineRoot = outlineRoot->clone(); + } + m_rootItem.reset(new PDFOutlineTreeItem(nullptr, qMove(outlineRoot))); } else @@ -425,13 +438,16 @@ Qt::ItemFlags PDFOutlineTreeItemModel::flags(const QModelIndex& index) const if (!index.isValid()) { + if (m_editable) + { + flags = flags | Qt::ItemIsDropEnabled; + } return flags; } - const PDFOutlineTreeItem* item = static_cast(index.internalPointer()); - if (item->getChildCount() == 0) + if (m_editable) { - flags = flags | Qt::ItemNeverHasChildren; + flags = flags | Qt::ItemIsEditable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled; } return flags; @@ -449,6 +465,259 @@ const PDFAction* PDFOutlineTreeItemModel::getAction(const QModelIndex& index) co return nullptr; } +const PDFOutlineItem* PDFOutlineTreeItemModel::getOutlineItem(const QModelIndex& index) const +{ + if (index.isValid()) + { + const PDFOutlineTreeItem* item = static_cast(index.internalPointer()); + const PDFOutlineItem* outlineItem = item->getOutlineItem(); + return outlineItem; + } + + return nullptr; +} + +PDFOutlineItem* PDFOutlineTreeItemModel::getOutlineItem(const QModelIndex& index) +{ + if (index.isValid()) + { + PDFOutlineTreeItem* item = static_cast(index.internalPointer()); + PDFOutlineItem* outlineItem = item->getOutlineItem(); + return outlineItem; + } + + return nullptr; +} + +void PDFOutlineTreeItemModel::setFontBold(const QModelIndex& index, bool value) +{ + if (PDFOutlineItem* outlineItem = getOutlineItem(index)) + { + if (outlineItem->isFontBold() != value) + { + outlineItem->setFontBold(value); + Q_EMIT dataChanged(index, index); + } + } +} + +void PDFOutlineTreeItemModel::setFontItalics(const QModelIndex& index, bool value) +{ + if (PDFOutlineItem* outlineItem = getOutlineItem(index)) + { + if (outlineItem->isFontItalics() != value) + { + outlineItem->setFontItalics(value); + Q_EMIT dataChanged(index, index); + } + } +} + +void PDFOutlineTreeItemModel::setDestination(const QModelIndex& index, const PDFDestination& destination) +{ + if (PDFOutlineItem* outlineItem = getOutlineItem(index)) + { + outlineItem->setAction(PDFActionPtr(new PDFActionGoTo(destination, PDFDestination()))); + Q_EMIT dataChanged(index, index); + } +} + +bool PDFOutlineTreeItemModel::setData(const QModelIndex& index, const QVariant& value, int role) +{ + if (!m_editable || !index.isValid() || role != Qt::EditRole) + { + return false; + } + + PDFOutlineTreeItem* item = static_cast(index.internalPointer()); + PDFOutlineItem* outlineItem = item->getOutlineItem(); + if (outlineItem->getTitle() != value.toString()) + { + outlineItem->setTitle(value.toString()); + Q_EMIT dataChanged(index, index); + } + + return true; +} + +Qt::DropActions PDFOutlineTreeItemModel::supportedDropActions() const +{ + if (!m_editable) + { + return Qt::IgnoreAction; + } + + return Qt::CopyAction | Qt::MoveAction; +} + +Qt::DropActions PDFOutlineTreeItemModel::supportedDragActions() const +{ + if (!m_editable) + { + return Qt::IgnoreAction; + } + + return Qt::CopyAction | Qt::MoveAction; +} + +QStringList PDFOutlineTreeItemModel::mimeTypes() const +{ + return QStringList() << "application/PDF4QT_PDFOutlineTreeItemModel"; +} + +QMimeData* PDFOutlineTreeItemModel::mimeData(const QModelIndexList& indexes) const +{ + QMimeData* mimeData = new QMimeData(); + + if (indexes.size() == 1) + { + QByteArray ba; + + { + QDataStream stream(&ba, QDataStream::WriteOnly); + stream << indexes.front().internalId(); + } + + mimeData->setData(mimeTypes().front(), ba); + } + + return mimeData; +} + +bool PDFOutlineTreeItemModel::canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const +{ + Q_UNUSED(row); + Q_UNUSED(column); + Q_UNUSED(parent); + + return action == Qt::MoveAction && data->hasFormat(mimeTypes().front()); +} + +bool PDFOutlineTreeItemModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) +{ + if (!data || action != Qt::MoveAction) + { + return false; + } + + QByteArray pointerData = data->data(mimeTypes().front()); + QDataStream stream(pointerData); + quintptr pointer = 0; + stream >> pointer; + + PDFOutlineTreeItem* item = reinterpret_cast(pointer); + QModelIndex sourceIndex = createIndex(item->getRow(), column, item); + return moveRow(sourceIndex.parent(), sourceIndex.row(), parent, row); +} + +bool PDFOutlineTreeItemModel::insertRows(int row, int count, const QModelIndex& parent) +{ + if (!m_editable || row < 0 || count <= 0 || row > rowCount(parent)) + { + return false; + } + + beginInsertRows(parent, row, row + count - 1); + + PDFOutlineTreeItem* item = parent.isValid() ? static_cast(parent.internalPointer()) : static_cast(m_rootItem.get()); + while (count > 0) + { + QSharedPointer outlineItem(new PDFOutlineItem()); + outlineItem->setTitle(tr("Item %1").arg(row + 1)); + PDFOutlineTreeItem* newTreeItem = new PDFOutlineTreeItem(item, qMove(outlineItem)); + item->insertCreatedChild(row, newTreeItem); + + ++row; + --count; + } + + endInsertRows(); + + return true; +} + +bool PDFOutlineTreeItemModel::removeRows(int row, int count, const QModelIndex& parent) +{ + if (!m_editable || count <= 0 || row < 0 || row + count > rowCount(parent)) + { + return false; + } + + beginRemoveRows(parent, row, row + count - 1); + + PDFOutlineTreeItem* item = parent.isValid() ? static_cast(parent.internalPointer()) : static_cast(m_rootItem.get()); + while (count > 0) + { + delete item->takeChild(row); + --count; + } + + endRemoveRows(); + + return false; +} + +bool PDFOutlineTreeItemModel::moveRows(const QModelIndex& sourceParent, int sourceRow, int count, const QModelIndex& destinationParent, int destinationChild) +{ + if (sourceRow < 0 || count <= 0 || (sourceParent == destinationParent && (sourceRow == destinationChild || sourceRow + count <= destinationChild))) + { + return false; + } + + PDFOutlineTreeItem* sourceNode = nullptr; + PDFOutlineTreeItem* destNode = nullptr; + + if (sourceParent.isValid()) + { + sourceNode = static_cast(sourceParent.internalPointer()); + } + else + { + sourceNode = static_cast(m_rootItem.get()); + } + + if (destinationParent.isValid()) + { + destNode = static_cast(destinationParent.internalPointer()); + } + else + { + destNode = static_cast(m_rootItem.get()); + } + + if (sourceRow + count > sourceNode->getChildCount()) + { + return false; + } + + if (destinationChild < 0) + { + destinationChild = 0; + } + + // Signalizace začátku přesunu řádků + if (!beginMoveRows(sourceParent, sourceRow, sourceRow + count - 1, destinationParent, destinationChild)) + { + return false; + } + + QList nodesToMove; + for (int i = 0; i < count; ++i) + { + nodesToMove.append(static_cast(sourceNode->takeChild(sourceRow))); + } + + for (PDFOutlineTreeItem* node : nodesToMove) + { + destNode->insertCreatedChild(destinationChild++, node); + } + + // Signalizace konce přesunu řádků + endMoveRows(); + + return true; +} + int PDFAttachmentsTreeItemModel::columnCount(const QModelIndex& parent) const { Q_UNUSED(parent); diff --git a/Pdf4QtLib/sources/pdfitemmodels.h b/Pdf4QtLib/sources/pdfitemmodels.h index 0222219..05344e4 100644 --- a/Pdf4QtLib/sources/pdfitemmodels.h +++ b/Pdf4QtLib/sources/pdfitemmodels.h @@ -36,6 +36,7 @@ class PDFModifiedDocument; class PDFFileSpecification; class PDFOptionalContentActivity; class PDFDrawWidgetProxy; +class PDFDestination; /// Represents tree item in the GUI tree class PDF4QTLIBSHARED_EXPORT PDFTreeItem @@ -53,6 +54,20 @@ public: return item; } + template + inline T* insertChild(int position, Arguments&&... arguments) + { + T* item = new T(this, std::forward(arguments)...); + m_children.insert(std::next(m_children.begin(), position), item); + return item; + } + + void insertCreatedChild(int position, PDFTreeItem* item) + { + item->m_parent = this; + m_children.insert(std::next(m_children.begin(), position), item); + } + void addCreatedChild(PDFTreeItem* item) { item->m_parent = this; @@ -64,6 +79,7 @@ public: const PDFTreeItem* getChild(int index) const { return m_children.at(index); } PDFTreeItem* getChild(int index) { return m_children.at(index); } const PDFTreeItem* getParent() const { return m_parent; } + PDFTreeItem* takeChild(int index); private: PDFTreeItem* m_parent = nullptr; @@ -145,6 +161,7 @@ public: explicit PDFOutlineTreeItem(PDFOutlineTreeItem* parent, QSharedPointer outlineItem); const PDFOutlineItem* getOutlineItem() const { return m_outlineItem.data(); } + PDFOutlineItem* getOutlineItem() { return m_outlineItem.data(); } private: QSharedPointer m_outlineItem; @@ -154,9 +171,10 @@ class PDF4QTLIBSHARED_EXPORT PDFOutlineTreeItemModel : public PDFTreeItemModel { Q_OBJECT public: - PDFOutlineTreeItemModel(QIcon icon, QObject* parent) : + PDFOutlineTreeItemModel(QIcon icon, bool editable, QObject* parent) : PDFTreeItemModel(parent), - m_icon(qMove(icon)) + m_icon(qMove(icon)), + m_editable(editable) { } @@ -165,14 +183,37 @@ public: 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; + virtual Qt::DropActions supportedDropActions() const override; + virtual Qt::DropActions supportedDragActions() const override; + virtual bool insertRows(int row, int count, const QModelIndex& parent) override; + virtual bool removeRows(int row, int count, const QModelIndex& parent) override; + virtual bool moveRows(const QModelIndex& sourceParent, int sourceRow, int count, const QModelIndex& destinationParent, int destinationChild) override; + virtual QStringList mimeTypes() const override; + virtual QMimeData* mimeData(const QModelIndexList& indexes) const override; + virtual bool canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const override; + virtual bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; /// Returns action assigned to the index. If index is invalid, or /// points to the invalid item, nullptr is returned. /// \param index Index of the outline item const PDFAction* getAction(const QModelIndex& index) const; + /// Returns the outline item for the given index, or nullptr + /// if no outline item is assigned to that index. + const PDFOutlineItem* getOutlineItem(const QModelIndex& index) const; + + /// Returns the outline item for the given index, or nullptr + /// if no outline item is assigned to that index. + PDFOutlineItem* getOutlineItem(const QModelIndex& index); + + void setFontBold(const QModelIndex& index, bool value); + void setFontItalics(const QModelIndex& index, bool value); + void setDestination(const QModelIndex& index, const PDFDestination& destination); + private: QIcon m_icon; + bool m_editable; }; class PDF4QTLIBSHARED_EXPORT PDFSelectableOutlineTreeItemModel : public PDFOutlineTreeItemModel @@ -184,7 +225,7 @@ private: public: PDFSelectableOutlineTreeItemModel(QIcon icon, QObject* parent) : - BaseClass(qMove(icon), parent) + BaseClass(qMove(icon), false, parent) { } diff --git a/Pdf4QtLib/sources/pdfoutline.cpp b/Pdf4QtLib/sources/pdfoutline.cpp index e88f4ad..fb54187 100644 --- a/Pdf4QtLib/sources/pdfoutline.cpp +++ b/Pdf4QtLib/sources/pdfoutline.cpp @@ -156,6 +156,29 @@ void PDFOutlineItem::apply(const std::function& functor) } } +QSharedPointer PDFOutlineItem::clone() const +{ + QSharedPointer result(new PDFOutlineItem()); + + result->setTitle(getTitle()); + result->setTextColor(getTextColor()); + result->setStructureElement(getStructureElement()); + result->setFontItalics(isFontItalics()); + result->setFontBold(isFontBold()); + + if (auto action = getAction()) + { + result->setAction(action->clone()); + } + + for (size_t i = 0; i < getChildCount(); ++i) + { + result->addChild(getChild(i)->clone()); + } + + return result; +} + bool PDFOutlineItem::isFontBold() const { return m_fontBold; diff --git a/Pdf4QtLib/sources/pdfoutline.h b/Pdf4QtLib/sources/pdfoutline.h index 85822cd..37479f3 100644 --- a/Pdf4QtLib/sources/pdfoutline.h +++ b/Pdf4QtLib/sources/pdfoutline.h @@ -65,6 +65,8 @@ public: void apply(const std::function& functor); + QSharedPointer clone() const; + private: static void parseImpl(const PDFObjectStorage* storage, PDFOutlineItem* parent, diff --git a/Pdf4QtViewer/pdfsidebarwidget.cpp b/Pdf4QtViewer/pdfsidebarwidget.cpp index c23617c..e06cb0d 100644 --- a/Pdf4QtViewer/pdfsidebarwidget.cpp +++ b/Pdf4QtViewer/pdfsidebarwidget.cpp @@ -23,6 +23,8 @@ #include "pdfwidgetutils.h" #include "pdftexttospeech.h" #include "pdfdbgheap.h" +#include "pdfdrawwidget.h" +#include "pdfwidgettool.h" #include "pdfdocument.h" #include "pdfitemmodels.h" @@ -53,6 +55,7 @@ PDFSidebarWidget::PDFSidebarWidget(pdf::PDFDrawWidgetProxy* proxy, PDFTextToSpeech* textToSpeech, pdf::PDFCertificateStore* certificateStore, PDFViewerSettings* settings, + bool editableOutline, QWidget* parent) : QWidget(parent), ui(new Ui::PDFSidebarWidget), @@ -73,9 +76,20 @@ PDFSidebarWidget::PDFSidebarWidget(pdf::PDFDrawWidgetProxy* proxy, // Outline QIcon bookmarkIcon(":/resources/bookmark.svg"); - m_outlineTreeModel = new pdf::PDFOutlineTreeItemModel(qMove(bookmarkIcon), this); + m_outlineTreeModel = new pdf::PDFOutlineTreeItemModel(qMove(bookmarkIcon), editableOutline, this); ui->bookmarksTreeView->setModel(m_outlineTreeModel); ui->bookmarksTreeView->header()->hide(); + + if (editableOutline) + { + ui->bookmarksTreeView->setDragEnabled(true); + ui->bookmarksTreeView->setAcceptDrops(true); + ui->bookmarksTreeView->setDropIndicatorShown(true); + ui->bookmarksTreeView->setDragDropMode(QAbstractItemView::InternalMove); + ui->bookmarksTreeView->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->bookmarksTreeView, &QTreeView::customContextMenuRequested, this, &PDFSidebarWidget::onBookmarksTreeViewContextMenuRequested); + } + connect(ui->bookmarksTreeView, &QTreeView::clicked, this, &PDFSidebarWidget::onOutlineItemClicked); // Thumbnails @@ -735,6 +749,119 @@ void PDFSidebarWidget::onSignatureCustomContextMenuRequested(const QPoint& pos) } } +void PDFSidebarWidget::onBookmarksTreeViewContextMenuRequested(const QPoint& pos) +{ + QMenu contextMenu; + + QModelIndex index = ui->bookmarksTreeView->indexAt(pos); + + auto onFollow = [this, index]() + { + onOutlineItemClicked(index); + }; + + auto onInsert = [this, index]() + { + if (index.isValid()) + { + ui->bookmarksTreeView->model()->insertRow(index.row() + 1, index.parent()); + } + else + { + ui->bookmarksTreeView->model()->insertRow(ui->bookmarksTreeView->model()->rowCount()); + } + }; + + auto onDelete = [this, index]() + { + ui->bookmarksTreeView->model()->removeRow(index.row(), index.parent()); + }; + + auto onRename = [this, index]() + { + ui->bookmarksTreeView->edit(index); + }; + + QAction* followAction = contextMenu.addAction(tr("Follow"), onFollow); + followAction->setEnabled(index.isValid()); + contextMenu.addSeparator(); + + QAction* deleteAction = contextMenu.addAction(tr("Delete"), onDelete); + QAction* insertAction = contextMenu.addAction(tr("Insert"), this, onInsert); + QAction* renameAction = contextMenu.addAction(tr("Rename"), this, onRename); + + deleteAction->setEnabled(index.isValid()); + insertAction->setEnabled(true); + renameAction->setEnabled(index.isValid()); + + contextMenu.addSeparator(); + + const pdf::PDFOutlineItem* outlineItem = m_outlineTreeModel->getOutlineItem(index); + const bool isFontBold = outlineItem && outlineItem->isFontBold(); + const bool isFontItalics = outlineItem && outlineItem->isFontItalics(); + + auto onFontBold = [this, index, isFontBold]() + { + m_outlineTreeModel->setFontBold(index, !isFontBold); + }; + + auto onFontItalic = [this, index, isFontItalics]() + { + m_outlineTreeModel->setFontItalics(index, !isFontItalics); + }; + + QAction* fontBoldAction = contextMenu.addAction(tr("Font Bold"), onFontBold); + QAction* fontItalicAction = contextMenu.addAction(tr("Font Italic"), onFontItalic); + fontBoldAction->setCheckable(true); + fontItalicAction->setCheckable(true); + fontBoldAction->setChecked(isFontBold); + fontItalicAction->setChecked(isFontItalics); + fontBoldAction->setEnabled(index.isValid()); + fontItalicAction->setEnabled(index.isValid()); + + QMenu* submenu = new QMenu(tr("Set Target"), &contextMenu); + QAction* targetAction = contextMenu.addMenu(submenu); + targetAction->setEnabled(index.isValid()); + + auto createOnSetTarget = [this, index](pdf::DestinationType destinationType) + { + auto onSetTarget = [this, index, destinationType]() + { + pdf::PDFToolManager* toolManager = m_proxy->getWidget()->getToolManager(); + + auto pickRectangle = [this, index, destinationType](pdf::PDFInteger pageIndex, QRectF rect) + { + pdf::PDFDestination destination; + destination.setDestinationType(destinationType); + destination.setPageIndex(pageIndex); + destination.setPageReference(m_document->getCatalog()->getPage(pageIndex)->getPageReference()); + destination.setLeft(rect.left()); + destination.setRight(rect.right()); + destination.setTop(rect.bottom()); + destination.setBottom(rect.top()); + destination.setZoom(m_proxy->getZoom()); + m_outlineTreeModel->setDestination(index, destination); + }; + + toolManager->pickRectangle(pickRectangle); + }; + + return onSetTarget; + }; + + submenu->addAction(tr("Named Destination")); + submenu->addAction(tr("Fit Page"), createOnSetTarget(pdf::DestinationType::Fit)); + submenu->addAction(tr("Fit Page Horizontally"), createOnSetTarget(pdf::DestinationType::FitH)); + submenu->addAction(tr("Fit Page Vertically"), createOnSetTarget(pdf::DestinationType::FitV)); + submenu->addAction(tr("Fit Rectangle"), createOnSetTarget(pdf::DestinationType::FitR)); + submenu->addAction(tr("Fit Bounding Box"), createOnSetTarget(pdf::DestinationType::FitB)); + submenu->addAction(tr("Fit Bounding Box Horizontally"), createOnSetTarget(pdf::DestinationType::FitBH)); + submenu->addAction(tr("Fit Bounding Box Vertically"), createOnSetTarget(pdf::DestinationType::FitBV)); + submenu->addAction(tr("XYZ"), createOnSetTarget(pdf::DestinationType::XYZ)); + + contextMenu.exec(ui->bookmarksTreeView->mapToGlobal(pos)); +} + void PDFSidebarWidget::paintEvent(QPaintEvent* event) { Q_UNUSED(event); diff --git a/Pdf4QtViewer/pdfsidebarwidget.h b/Pdf4QtViewer/pdfsidebarwidget.h index 0aa30f3..4ecac46 100644 --- a/Pdf4QtViewer/pdfsidebarwidget.h +++ b/Pdf4QtViewer/pdfsidebarwidget.h @@ -62,6 +62,7 @@ public: PDFTextToSpeech* textToSpeech, pdf::PDFCertificateStore* certificateStore, PDFViewerSettings* settings, + bool editableOutline, QWidget* parent); virtual ~PDFSidebarWidget() override; @@ -111,6 +112,7 @@ private: void onAttachmentCustomContextMenuRequested(const QPoint& pos); void onThumbnailClicked(const QModelIndex& index); void onSignatureCustomContextMenuRequested(const QPoint &pos); + void onBookmarksTreeViewContextMenuRequested(const QPoint &pos); struct PageInfo { diff --git a/Pdf4QtViewer/pdfviewermainwindow.cpp b/Pdf4QtViewer/pdfviewermainwindow.cpp index 88c8d8c..889bb34 100644 --- a/Pdf4QtViewer/pdfviewermainwindow.cpp +++ b/Pdf4QtViewer/pdfviewermainwindow.cpp @@ -253,7 +253,7 @@ PDFViewerMainWindow::PDFViewerMainWindow(QWidget* parent) : setCentralWidget(m_programController->getPdfWidget()); setFocusProxy(m_programController->getPdfWidget()); - m_sidebarWidget = new PDFSidebarWidget(m_programController->getPdfWidget()->getDrawWidgetProxy(), m_programController->getTextToSpeech(), m_programController->getCertificateStore(), m_programController->getSettings(), this); + m_sidebarWidget = new PDFSidebarWidget(m_programController->getPdfWidget()->getDrawWidgetProxy(), m_programController->getTextToSpeech(), m_programController->getCertificateStore(), m_programController->getSettings(), true, this); m_sidebarDockWidget = new QDockWidget(tr("Sidebar"), this); m_sidebarDockWidget->setObjectName("SidebarDockWidget"); m_sidebarDockWidget->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); diff --git a/Pdf4QtViewer/pdfviewermainwindowlite.cpp b/Pdf4QtViewer/pdfviewermainwindowlite.cpp index bd7dd0b..392d79f 100644 --- a/Pdf4QtViewer/pdfviewermainwindowlite.cpp +++ b/Pdf4QtViewer/pdfviewermainwindowlite.cpp @@ -188,7 +188,7 @@ PDFViewerMainWindowLite::PDFViewerMainWindowLite(QWidget* parent) : setCentralWidget(m_programController->getPdfWidget()); setFocusProxy(m_programController->getPdfWidget()); - m_sidebarWidget = new PDFSidebarWidget(m_programController->getPdfWidget()->getDrawWidgetProxy(), m_programController->getTextToSpeech(), m_programController->getCertificateStore(), m_programController->getSettings(), this); + m_sidebarWidget = new PDFSidebarWidget(m_programController->getPdfWidget()->getDrawWidgetProxy(), m_programController->getTextToSpeech(), m_programController->getCertificateStore(), m_programController->getSettings(), false, this); m_sidebarDockWidget = new QDockWidget(tr("Sidebar"), this); m_sidebarDockWidget->setObjectName("SidebarDockWidget"); m_sidebarDockWidget->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);