DocPage Organizer: Undo/Redo

This commit is contained in:
Jakub Melka 2021-08-01 11:19:14 +02:00
parent 089f7244f0
commit 7a40427d10
8 changed files with 403 additions and 0 deletions

View File

@ -60,6 +60,8 @@ MainWindow::MainWindow(QWidget* parent) :
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));
@ -96,6 +98,8 @@ MainWindow::MainWindow(QWidget* parent) :
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 });
@ -303,6 +307,12 @@ bool MainWindow::canPerformOperation(Operation operation) const
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;
@ -417,6 +427,18 @@ void MainWindow::performOperation(Operation operation)
break;
}
case Operation::Undo:
{
m_model->undo();
break;
}
case Operation::Redo:
{
m_model->redo();
break;
}
case Operation::Cut:
case Operation::Copy:
{

View File

@ -53,6 +53,9 @@ public:
ReplaceSelection,
RestoreRemovedItems,
Undo,
Redo,
Cut,
Copy,
Paste,

View File

@ -63,6 +63,9 @@
<property name="title">
<string>Edit</string>
</property>
<addaction name="actionUndo"/>
<addaction name="actionRedo"/>
<addaction name="separator"/>
<addaction name="actionCloneSelection"/>
<addaction name="actionRemoveSelection"/>
<addaction name="actionReplaceSelection"/>
@ -555,6 +558,30 @@
<string>Invert Selection</string>
</property>
</action>
<action name="actionUndo">
<property name="icon">
<iconset resource="resources.qrc">
<normaloff>:/pdfdocpage/resources/undo.svg</normaloff>:/pdfdocpage/resources/undo.svg</iconset>
</property>
<property name="text">
<string>Undo</string>
</property>
<property name="shortcut">
<string>Ctrl+Z</string>
</property>
</action>
<action name="actionRedo">
<property name="icon">
<iconset resource="resources.qrc">
<normaloff>:/pdfdocpage/resources/redo.svg</normaloff>:/pdfdocpage/resources/redo.svg</iconset>
</property>
<property name="text">
<string>Redo</string>
</property>
<property name="shortcut">
<string>Ctrl+Y</string>
</property>
</action>
</widget>
<resources>
<include location="resources.qrc"/>

View File

@ -97,6 +97,7 @@ QVariant PageItemModel::data(const QModelIndex& index, int role) const
int PageItemModel::insertDocument(QString fileName, pdf::PDFDocument document, const QModelIndex& index)
{
Modifier modifier(this);
auto it = std::find_if(m_documents.cbegin(), m_documents.cend(), [&](const auto& item) { return item.second.fileName == fileName; });
if (it != m_documents.cend())
{
@ -117,6 +118,7 @@ int PageItemModel::insertDocument(QString fileName, pdf::PDFDocument document, c
int PageItemModel::insertImage(QString fileName, const QModelIndex& index)
{
Modifier modifier(this);
QFile file(fileName);
if (file.open(QFile::ReadOnly))
@ -234,6 +236,8 @@ void PageItemModel::group(const QModelIndexList& list)
return;
}
Modifier modifier(this);
std::vector<size_t> groupedIndices;
groupedIndices.reserve(list.size());
std::transform(list.cbegin(), list.cend(), std::back_inserter(groupedIndices), [](const auto& index) { return index.row(); });
@ -275,6 +279,8 @@ void PageItemModel::ungroup(const QModelIndexList& list)
return;
}
Modifier modifier(this);
std::vector<size_t> ungroupedIndices;
ungroupedIndices.reserve(list.size());
std::transform(list.cbegin(), list.cend(), std::back_inserter(ungroupedIndices), [](const auto& index) { return index.row(); });
@ -319,6 +325,8 @@ QModelIndexList PageItemModel::restoreRemovedItems()
return result;
}
Modifier modifier(this);
const int trashBinSize = int(m_trashBin.size());
const int rowCount = this->rowCount(QModelIndex());
beginInsertRows(QModelIndex(), rowCount, rowCount + trashBinSize - 1);
@ -345,6 +353,8 @@ QModelIndexList PageItemModel::cloneSelection(const QModelIndexList& list)
return result;
}
Modifier modifier(this);
std::vector<int> rows;
rows.reserve(list.size());
std::transform(list.cbegin(), list.cend(), std::back_inserter(rows), [](const auto& index) { return index.row(); });
@ -381,6 +391,8 @@ void PageItemModel::removeSelection(const QModelIndexList& list)
return;
}
Modifier modifier(this);
std::vector<int> rows;
rows.reserve(list.size());
std::transform(list.cbegin(), list.cend(), std::back_inserter(rows), [](const auto& index) { return index.row(); });
@ -397,6 +409,8 @@ void PageItemModel::removeSelection(const QModelIndexList& list)
void PageItemModel::insertEmptyPage(const QModelIndexList& list)
{
Modifier modifier(this);
if (list.isEmpty())
{
insertEmptyPage(QModelIndex());
@ -461,6 +475,8 @@ void PageItemModel::rotateLeft(const QModelIndexList& list)
return;
}
Modifier modifier(this);
int rowMin = list.front().row();
int rowMax = list.front().row();
@ -488,6 +504,8 @@ void PageItemModel::rotateRight(const QModelIndexList& list)
return;
}
Modifier modifier(this);
int rowMin = list.front().row();
int rowMax = list.front().row();
@ -560,6 +578,8 @@ void PageItemModel::regroupEvenOdd(const QModelIndexList& list)
return;
}
Modifier modifier(this);
std::vector<PageGroupItem> pageGroupItems = m_pageGroupItems;
std::vector<PageGroupItem::GroupItem> extractedItems = extractItems(pageGroupItems, list);
@ -598,6 +618,8 @@ void PageItemModel::regroupPaired(const QModelIndexList& list)
return;
}
Modifier modifier(this);
std::vector<PageGroupItem> pageGroupItems = m_pageGroupItems;
std::vector<PageGroupItem::GroupItem> extractedItems = extractItems(pageGroupItems, list);
@ -627,6 +649,8 @@ void PageItemModel::regroupPaired(const QModelIndexList& list)
void PageItemModel::regroupBookmarks(const QModelIndexList& list)
{
Q_ASSERT(false);
Modifier modifier(this);
}
void PageItemModel::regroupAlternatingPages(const QModelIndexList& list, bool reversed)
@ -636,6 +660,8 @@ void PageItemModel::regroupAlternatingPages(const QModelIndexList& list, bool re
return;
}
Modifier modifier(this);
std::vector<PageGroupItem> pageGroupItems = m_pageGroupItems;
std::vector<PageGroupItem::GroupItem> extractedItems = extractItems(pageGroupItems, list);
const int documentIndex = extractedItems.front().documentIndex;
@ -678,6 +704,35 @@ void PageItemModel::regroupAlternatingPages(const QModelIndexList& list, bool re
}
}
void PageItemModel::undo()
{
performUndoRedo(m_undoSteps, m_redoSteps);
}
void PageItemModel::performUndoRedo(std::vector<PageItemModel::UndoRedoStep>& load,
std::vector<PageItemModel::UndoRedoStep>& save)
{
if (load.empty())
{
return;
}
save.emplace_back(getCurrentStep());
UndoRedoStep step = std::move(load.back());
load.pop_back();
updateUndoRedoSteps();
beginResetModel();
m_pageGroupItems = std::move(step.pageGroupItems);
m_trashBin = std::move(step.trashBin);
endResetModel();
}
void PageItemModel::redo()
{
performUndoRedo(m_redoSteps, m_undoSteps);
}
QItemSelection PageItemModel::getSelectionImpl(std::function<bool (const PageGroupItem::GroupItem&)> filter) const
{
QItemSelection result;
@ -707,6 +762,25 @@ QItemSelection PageItemModel::getSelectionImpl(std::function<bool (const PageGro
return result;
}
void PageItemModel::updateUndoRedoSteps()
{
while (m_undoSteps.size() > MAX_UNDO_REDO_STEPS)
{
m_undoSteps.erase(m_undoSteps.begin());
}
while (m_redoSteps.size() > MAX_UNDO_REDO_STEPS)
{
m_redoSteps.erase(m_redoSteps.begin());
}
}
void PageItemModel::clearUndoRedo()
{
m_undoSteps.clear();
m_redoSteps.clear();
}
void PageItemModel::createDocumentGroup(int index, const QModelIndex& insertIndex)
{
const DocumentItem& item = m_documents.at(index);
@ -939,6 +1013,8 @@ bool PageItemModel::dropMimeData(const QMimeData* data, Qt::DropAction action, i
return false;
}
Modifier modifier(this);
int insertRow = rowCount(QModelIndex());
if (row > -1)
{
@ -1140,7 +1216,28 @@ void PageItemModel::clear()
m_pageGroupItems.clear();
m_documents.clear();
m_trashBin.clear();
clearUndoRedo();
endResetModel();
}
PageItemModel::Modifier::Modifier(PageItemModel* model) :
m_model(model)
{
Q_ASSERT(model);
m_stateBeforeModification = m_model->getCurrentStep();
}
PageItemModel::Modifier::~Modifier()
{
UndoRedoStep stateAfterModification = m_model->getCurrentStep();
if (m_stateBeforeModification != stateAfterModification)
{
m_model->m_undoSteps.emplace_back(std::move(m_stateBeforeModification));
m_model->m_redoSteps.clear();
m_model->updateUndoRedoSteps();
}
}
} // namespace pdfdocpage

View File

@ -186,20 +186,56 @@ public:
void regroupBookmarks(const QModelIndexList& list);
void regroupAlternatingPages(const QModelIndexList& list, bool reversed);
bool canUndo() const { return !m_undoSteps.empty(); }
bool canRedo() const { return !m_redoSteps.empty(); }
void undo();
void redo();
private:
static const int MAX_UNDO_REDO_STEPS = 10;
void createDocumentGroup(int index, const QModelIndex& insertIndex);
QString getGroupNameFromDocument(int index) const;
void updateItemCaptionAndTags(PageGroupItem& item) const;
void insertEmptyPage(const QModelIndex& index);
struct UndoRedoStep
{
auto operator<=>(const UndoRedoStep&) const = default;
std::vector<PageGroupItem> pageGroupItems;
std::vector<PageGroupItem> trashBin;
};
class Modifier
{
public:
explicit Modifier(PageItemModel* model);
~Modifier();
private:
PageItemModel* m_model;
UndoRedoStep m_stateBeforeModification;
};
std::vector<PageGroupItem::GroupItem> extractItems(std::vector<PageGroupItem>& items, const QModelIndexList& selection) const;
QItemSelection getSelectionImpl(std::function<bool(const PageGroupItem::GroupItem&)> filter) const;
UndoRedoStep getCurrentStep() const { return UndoRedoStep{ m_pageGroupItems, m_trashBin }; }
void updateUndoRedoSteps();
void clearUndoRedo();
void performUndoRedo(std::vector<UndoRedoStep>& load, std::vector<UndoRedoStep>& save);
std::vector<PageGroupItem> m_pageGroupItems;
std::map<int, DocumentItem> m_documents;
std::map<int, ImageItem> m_images;
std::vector<PageGroupItem> m_trashBin;
std::vector<UndoRedoStep> m_undoSteps; // Oldest step is first, newest step is last
std::vector<UndoRedoStep> m_redoSteps;
};
} // namespace pdfdocpage

View File

@ -36,5 +36,7 @@
<file>resources/regroup-bookmarks.svg</file>
<file>resources/regroup-even-odd.svg</file>
<file>resources/regroup-pairs.svg</file>
<file>resources/undo.svg</file>
<file>resources/redo.svg</file>
</qresource>
</RCC>

View File

@ -0,0 +1,108 @@
<?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="select-odd.svg">
<defs
id="defs5285">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 15 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="30 : 15 : 1"
inkscape:persp3d-origin="15 : 10 : 1"
id="perspective5921" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.656854"
inkscape:cx="148.43961"
inkscape:cy="135.18316"
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)">
<flowRoot
xml:space="preserve"
id="flowRoot5913"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
id="flowRegion5915"><rect
id="rect5917"
width="129.22377"
height="91.747108"
x="-13.788582"
y="-33.515606" /></flowRegion><flowPara
id="flowPara5919" /></flowRoot> <g
aria-label="?"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:25.39999962px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
id="text849"
transform="translate(-4.7625002,-4.2333335)">
<path
d="m 25.373474,281.70442 q 0,1.21543 -0.434082,2.17041 -0.434082,0.94258 -1.141016,1.67432 -0.694531,0.70693 -1.599902,1.32705 -0.905371,0.62011 -1.922363,1.20302 v 2.79053 h -2.22002 v -3.78271 q 0.806153,-0.45889 1.736328,-1.00459 0.942578,-0.54571 1.537891,-1.10381 0.719336,-0.64492 1.116211,-1.32705 0.396875,-0.69453 0.396875,-1.76114 0,-1.40146 -0.954981,-2.08359 -0.942578,-0.69453 -2.443261,-0.69453 -1.339453,0 -2.542481,0.42168 -1.190625,0.42168 -1.885156,0.85576 h -0.124023 v -2.53008 q 0.868164,-0.33486 2.195214,-0.59531 1.339454,-0.27285 2.530079,-0.27285 2.666503,0 4.204394,1.30224 1.550293,1.28985 1.550293,3.41065 z m -4.898926,14.12627 H 17.94447 v -2.6169 h 2.530078 z"
style="stroke-width:0.26458332"
id="path851"
inkscape:connector-curvature="0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -0,0 +1,108 @@
<?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="select-odd.svg">
<defs
id="defs5285">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 15 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="30 : 15 : 1"
inkscape:persp3d-origin="15 : 10 : 1"
id="perspective5921" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.656854"
inkscape:cx="148.43961"
inkscape:cy="135.18316"
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)">
<flowRoot
xml:space="preserve"
id="flowRoot5913"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
id="flowRegion5915"><rect
id="rect5917"
width="129.22377"
height="91.747108"
x="-13.788582"
y="-33.515606" /></flowRegion><flowPara
id="flowPara5919" /></flowRoot> <g
aria-label="?"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:25.39999962px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
id="text849"
transform="translate(-4.7625002,-4.2333335)">
<path
d="m 25.373474,281.70442 q 0,1.21543 -0.434082,2.17041 -0.434082,0.94258 -1.141016,1.67432 -0.694531,0.70693 -1.599902,1.32705 -0.905371,0.62011 -1.922363,1.20302 v 2.79053 h -2.22002 v -3.78271 q 0.806153,-0.45889 1.736328,-1.00459 0.942578,-0.54571 1.537891,-1.10381 0.719336,-0.64492 1.116211,-1.32705 0.396875,-0.69453 0.396875,-1.76114 0,-1.40146 -0.954981,-2.08359 -0.942578,-0.69453 -2.443261,-0.69453 -1.339453,0 -2.542481,0.42168 -1.190625,0.42168 -1.885156,0.85576 h -0.124023 v -2.53008 q 0.868164,-0.33486 2.195214,-0.59531 1.339454,-0.27285 2.530079,-0.27285 2.666503,0 4.204394,1.30224 1.550293,1.28985 1.550293,3.41065 z m -4.898926,14.12627 H 17.94447 v -2.6169 h 2.530078 z"
style="stroke-width:0.26458332"
id="path851"
inkscape:connector-curvature="0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB