diff --git a/glabels/CMakeLists.txt b/glabels/CMakeLists.txt index d070ff0..1121a3c 100644 --- a/glabels/CMakeLists.txt +++ b/glabels/CMakeLists.txt @@ -51,6 +51,7 @@ set (glabels_sources TemplatePicker.cpp TemplatePickerItem.cpp TextNode.cpp + UndoRedoModel.cpp XmlLabelCreator.cpp XmlLabelParser.cpp ) @@ -87,6 +88,7 @@ set (glabels_qobject_headers SimplePreview.h StartupWizard.h TemplatePicker.h + UndoRedoModel.h ) set (glabels_forms diff --git a/glabels/LabelEditor.cpp b/glabels/LabelEditor.cpp index 3383e48..338627c 100644 --- a/glabels/LabelEditor.cpp +++ b/glabels/LabelEditor.cpp @@ -28,6 +28,7 @@ #include "LabelModel.h" #include "LabelModelObject.h" #include "LabelModelBoxObject.h" +#include "UndoRedoModel.h" #include "Settings.h" #include "Cursors.h" @@ -81,6 +82,7 @@ LabelEditor::LabelEditor( QScrollArea* scrollArea, QWidget* parent ) mState = IdleState; mModel = 0; + mUndoRedoModel = 0; mMarkupVisible = true; mGridVisible = true; mGridSpacing = 18; @@ -127,9 +129,10 @@ LabelEditor::qridVisible() const /// Model Parameter Setter /// void -LabelEditor::setModel( LabelModel* model ) +LabelEditor::setModel( LabelModel* model, UndoRedoModel* undoRedoModel ) { mModel = model; + mUndoRedoModel = undoRedoModel; if ( model ) { @@ -571,6 +574,7 @@ LabelEditor::mouseMoveEvent( QMouseEvent* event ) break; case ArrowMove: + mUndoRedoModel->checkpoint( tr("Move") ); mModel->moveSelection( (xWorld - mMoveLastX), (yWorld - mMoveLastY) ); mMoveLastX = xWorld; @@ -867,22 +871,27 @@ LabelEditor::keyPressEvent( QKeyEvent* event ) { case Qt::Key_Left: + mUndoRedoModel->checkpoint( tr("Move") ); mModel->moveSelection( -mStepSize, glabels::Distance(0) ); break; case Qt::Key_Up: + mUndoRedoModel->checkpoint( tr("Move") ); mModel->moveSelection( glabels::Distance(0), -mStepSize ); break; case Qt::Key_Right: + mUndoRedoModel->checkpoint( tr("Move") ); mModel->moveSelection( mStepSize, glabels::Distance(0) ); break; case Qt::Key_Down: + mUndoRedoModel->checkpoint( tr("Move") ); mModel->moveSelection( glabels::Distance(0), mStepSize ); break; case Qt::Key_Delete: + mUndoRedoModel->checkpoint( tr("Delete") ); mModel->deleteSelection(); setCursor( Qt::ArrowCursor ); break; diff --git a/glabels/LabelEditor.h b/glabels/LabelEditor.h index d58f1f3..49ee1e2 100644 --- a/glabels/LabelEditor.h +++ b/glabels/LabelEditor.h @@ -31,6 +31,7 @@ // Forward References class LabelModel; class LabelModelObject; +class UndoRedoModel; class Handle; @@ -72,7 +73,7 @@ public: // Model ///////////////////////////////////// public: - void setModel( LabelModel* model ); + void setModel( LabelModel* model, UndoRedoModel* undoRedoModel ); ///////////////////////////////////// @@ -184,6 +185,7 @@ private: glabels::Distance mStepSize; LabelModel* mModel; + UndoRedoModel* mUndoRedoModel; State mState; diff --git a/glabels/MainWindow.cpp b/glabels/MainWindow.cpp index af6b779..579ecdd 100644 --- a/glabels/MainWindow.cpp +++ b/glabels/MainWindow.cpp @@ -44,6 +44,7 @@ #include "PrintView.h" #include "LabelModel.h" #include "LabelModelBoxObject.h" +#include "UndoRedoModel.h" #include "Icons.h" #include "File.h" #include "Help.h" @@ -163,8 +164,10 @@ LabelModel* MainWindow::model() const void MainWindow::setModel( LabelModel *label ) { mModel = label; - mPropertiesView->setModel( mModel ); - mLabelEditor->setModel( mModel ); + mUndoRedoModel = new UndoRedoModel( mModel ); + + mPropertiesView->setModel( mModel, mUndoRedoModel ); + mLabelEditor->setModel( mModel, mUndoRedoModel ); mObjectEditor->setModel( mModel ); mPrintView->setModel( mModel ); @@ -180,6 +183,7 @@ void MainWindow::setModel( LabelModel *label ) connect( mModel, SIGNAL(modifiedChanged()), this, SLOT(onModifiedChanged()) ); connect( mModel, SIGNAL(selectionChanged()), this, SLOT(onSelectionChanged()) ); connect( mModel, SIGNAL(changed()), this, SLOT(onLabelChanged()) ); + connect( mUndoRedoModel, SIGNAL(changed()), this, SLOT(onUndoRedoChanged()) ); } @@ -726,8 +730,8 @@ void MainWindow::setDocVerbsEnabled( bool enabled ) { fileSaveAction->setEnabled( enabled ); fileSaveAsAction->setEnabled( enabled ); - editUndoAction->setEnabled( enabled ); - editRedoAction->setEnabled( enabled ); + editUndoAction->setEnabled( enabled && mUndoRedoModel->canUndo() ); + editRedoAction->setEnabled( enabled && mUndoRedoModel->canRedo() ); editDeleteAction->setEnabled( enabled ); editSelectAllAction->setEnabled( enabled ); editUnSelectAllAction->setEnabled( enabled ); @@ -979,7 +983,7 @@ void MainWindow::fileExit() /// void MainWindow::editUndo() { - qDebug() << "ACTION: edit->Undo"; + mUndoRedoModel->undo(); } @@ -988,7 +992,7 @@ void MainWindow::editUndo() /// void MainWindow::editRedo() { - qDebug() << "ACTION: edit->Redo"; + mUndoRedoModel->redo(); } @@ -997,6 +1001,7 @@ void MainWindow::editRedo() /// void MainWindow::editCut() { + mUndoRedoModel->checkpoint( tr("Cut") ); mModel->cutSelection(); } @@ -1006,6 +1011,7 @@ void MainWindow::editCut() /// void MainWindow::editCopy() { + // Non-destructive -- do not checkpoint. mModel->copySelection(); } @@ -1015,6 +1021,7 @@ void MainWindow::editCopy() /// void MainWindow::editPaste() { + mUndoRedoModel->checkpoint( tr("Paste") ); mModel->paste(); } @@ -1024,6 +1031,7 @@ void MainWindow::editPaste() /// void MainWindow::editDelete() { + mUndoRedoModel->checkpoint( tr("Delete") ); mModel->deleteSelection(); } @@ -1151,6 +1159,7 @@ void MainWindow::objectsCreateText() /// void MainWindow::objectsCreateBox() { + mUndoRedoModel->checkpoint( tr("Create Box") ); mLabelEditor->createBoxMode(); } @@ -1196,6 +1205,7 @@ void MainWindow::objectsCreateBarcode() /// void MainWindow::objectsOrderRaise() { + mUndoRedoModel->checkpoint( tr("Bring To Front") ); mModel->raiseSelectionToTop(); } @@ -1205,6 +1215,7 @@ void MainWindow::objectsOrderRaise() /// void MainWindow::objectsOrderLower() { + mUndoRedoModel->checkpoint( tr("Send To Back") ); mModel->lowerSelectionToBottom(); } @@ -1214,6 +1225,7 @@ void MainWindow::objectsOrderLower() /// void MainWindow::objectsXformRotateLeft() { + mUndoRedoModel->checkpoint( tr("Rotate Left") ); mModel->rotateSelectionLeft(); } @@ -1223,6 +1235,7 @@ void MainWindow::objectsXformRotateLeft() /// void MainWindow::objectsXformRotateRight() { + mUndoRedoModel->checkpoint( tr("Rotate Right") ); mModel->rotateSelectionRight(); } @@ -1232,6 +1245,7 @@ void MainWindow::objectsXformRotateRight() /// void MainWindow::objectsXformFlipHoriz() { + mUndoRedoModel->checkpoint( tr("Flip Horizontally") ); mModel->flipSelectionHoriz(); } @@ -1241,6 +1255,7 @@ void MainWindow::objectsXformFlipHoriz() /// void MainWindow::objectsXformFlipVert() { + mUndoRedoModel->checkpoint( tr("Flip Vertically") ); mModel->flipSelectionVert(); } @@ -1250,6 +1265,7 @@ void MainWindow::objectsXformFlipVert() /// void MainWindow::objectsAlignLeft() { + mUndoRedoModel->checkpoint( tr("Align Left") ); mModel->alignSelectionLeft(); } @@ -1259,6 +1275,7 @@ void MainWindow::objectsAlignLeft() /// void MainWindow::objectsAlignHCenter() { + mUndoRedoModel->checkpoint( tr("Align Center") ); mModel->alignSelectionHCenter(); } @@ -1268,6 +1285,7 @@ void MainWindow::objectsAlignHCenter() /// void MainWindow::objectsAlignRight() { + mUndoRedoModel->checkpoint( tr("Align Right") ); mModel->alignSelectionRight(); } @@ -1277,6 +1295,7 @@ void MainWindow::objectsAlignRight() /// void MainWindow::objectsAlignTop() { + mUndoRedoModel->checkpoint( tr("Align Top") ); mModel->alignSelectionTop(); } @@ -1286,6 +1305,7 @@ void MainWindow::objectsAlignTop() /// void MainWindow::objectsAlignVCenter() { + mUndoRedoModel->checkpoint( tr("Align Middle") ); mModel->alignSelectionVCenter(); } @@ -1295,6 +1315,7 @@ void MainWindow::objectsAlignVCenter() /// void MainWindow::objectsAlignBottom() { + mUndoRedoModel->checkpoint( tr("Align Bottom") ); mModel->alignSelectionBottom(); } @@ -1304,6 +1325,7 @@ void MainWindow::objectsAlignBottom() /// void MainWindow::objectsCenterHoriz() { + mUndoRedoModel->checkpoint( tr("Center Horizontally") ); mModel->centerSelectionHoriz(); } @@ -1313,6 +1335,7 @@ void MainWindow::objectsCenterHoriz() /// void MainWindow::objectsCenterVert() { + mUndoRedoModel->checkpoint( tr("Center Vertically") ); mModel->centerSelectionVert(); } @@ -1416,5 +1439,14 @@ void MainWindow::onSelectionChanged() /// void MainWindow::onLabelChanged() { - /* @TODO: update undo/redo verbs. */ +} + + +/// +/// Undo/Redo changed handler +/// +void MainWindow::onUndoRedoChanged() +{ + editUndoAction->setEnabled( mUndoRedoModel->canUndo() ); + editRedoAction->setEnabled( mUndoRedoModel->canRedo() ); } diff --git a/glabels/MainWindow.h b/glabels/MainWindow.h index b17f6bb..98ddd21 100644 --- a/glabels/MainWindow.h +++ b/glabels/MainWindow.h @@ -37,6 +37,7 @@ class QScrollArea; // Forward References class LabelModel; +class UndoRedoModel; class PropertiesView; class LabelEditor; class ObjectEditor; @@ -148,6 +149,7 @@ private slots: void onModifiedChanged(); void onSelectionChanged(); void onLabelChanged(); + void onUndoRedoChanged(); ///////////////////////////////////// @@ -204,9 +206,11 @@ private: QToolBar* fileToolBar; QToolBar* editorToolBar; + LabelModel* mModel; + UndoRedoModel* mUndoRedoModel; + QListWidget* mContents; QStackedWidget* mPages; - LabelModel* mModel; PropertiesView* mPropertiesView; QScrollArea* mLabelEditorScrollArea; LabelEditor* mLabelEditor; diff --git a/glabels/PropertiesView.cpp b/glabels/PropertiesView.cpp index d2e051e..4f85cf6 100644 --- a/glabels/PropertiesView.cpp +++ b/glabels/PropertiesView.cpp @@ -21,6 +21,7 @@ #include "PropertiesView.h" #include "LabelModel.h" +#include "UndoRedoModel.h" #include "Settings.h" #include "libglabels/Db.h" @@ -55,9 +56,10 @@ PropertiesView::~PropertiesView() /// /// Set Model /// -void PropertiesView::setModel( LabelModel* model ) +void PropertiesView::setModel( LabelModel* model, UndoRedoModel* undoRedoModel ) { mModel = model; + mUndoRedoModel = undoRedoModel; connect( mModel, SIGNAL(sizeChanged()), this, SLOT(onLabelSizeChanged()) ); @@ -171,11 +173,13 @@ void PropertiesView::onFormChanged() } else if ( frame->w() > frame->h() ) { + mUndoRedoModel->checkpoint( tr("Product Orientation") ); int index = orientationCombo->currentIndex(); mModel->setRotate( index == 1 ); } else { + mUndoRedoModel->checkpoint( tr("Product Orientation") ); int index = orientationCombo->currentIndex(); mModel->setRotate( index == 0 ); } @@ -193,6 +197,8 @@ void PropertiesView::onChangeProductButtonClicked() const glabels::Template* tmplate = selectProductDialog.tmplate(); if ( tmplate ) { + mUndoRedoModel->checkpoint( tr("Change Product") ); + mModel->setTmplate( tmplate ); // Don't rotate circular or round labels diff --git a/glabels/PropertiesView.h b/glabels/PropertiesView.h index 04f9dde..dceb775 100644 --- a/glabels/PropertiesView.h +++ b/glabels/PropertiesView.h @@ -25,7 +25,9 @@ #include "libglabels/Units.h" -class LabelModel; // Forward reference +// Forward references +class LabelModel; +class UndoRedoModel; /// @@ -47,7 +49,7 @@ public: ///////////////////////////////// // Public methods ///////////////////////////////// - void setModel( LabelModel* model ); + void setModel( LabelModel* model, UndoRedoModel* undoRedoModel ); ///////////////////////////////// @@ -65,6 +67,7 @@ private slots: ///////////////////////////////// private: LabelModel* mModel; + UndoRedoModel* mUndoRedoModel; glabels::Units mUnits; }; diff --git a/glabels/UndoRedoModel.cpp b/glabels/UndoRedoModel.cpp new file mode 100644 index 0000000..d47520a --- /dev/null +++ b/glabels/UndoRedoModel.cpp @@ -0,0 +1,255 @@ +/* UndoRedoModel.cpp + * + * Copyright (C) 2016 Jim Evins + * + * This file is part of gLabels-qt. + * + * gLabels-qt is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * gLabels-qt 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with gLabels-qt. If not, see . + */ + +#include "UndoRedoModel.h" + +#include "LabelModel.h" + + +/// +/// Constructor +/// +UndoRedoModel::UndoRedoModel( LabelModel* model ) +{ + mModel = model; + mNewSelection = true; + + connect( model, SIGNAL(selectionChanged()), this, SLOT(onSelectionChanged()) ); +} + + +/// +/// Destructor +/// +UndoRedoModel::~UndoRedoModel() +{ +} + + +/// +/// Checkpoint +/// +void UndoRedoModel::checkpoint( const QString& description ) +{ + // + // Do not perform consecutive checkpoints that are identical. + // E.g. moving an object by dragging, would produce a large number + // of incremental checkpoints -- what we really want is a single + // checkpoint so that we can undo the entire dragging effort with + // one "undo" + // + if ( mNewSelection || (description != mLastDescription) ) + { + + /* Sever old redo "thread" */ + mRedoStack.clear(); + + /* Save state onto undo stack. */ + State* stateNow = new State( mModel, description ); + mUndoStack.push( stateNow ); + + /* Track consecutive checkpoints. */ + mNewSelection = false; + mLastDescription = description; + + emit changed(); + } +} + + +/// +/// Undo +/// +void UndoRedoModel::undo() +{ + State* oldState = mUndoStack.pop(); + State* stateNow = new State( mModel, oldState->description ); + + mRedoStack.push( stateNow ); + + mModel->restore( oldState->model ); + delete oldState; + + mNewSelection = true; + + emit changed(); +} + + +/// +/// Redo +/// +void UndoRedoModel::redo() +{ + State* oldState = mRedoStack.pop(); + State* stateNow = new State( mModel, oldState->description ); + + mUndoStack.push( stateNow ); + + mModel->restore( oldState->model ); + delete oldState; + + mNewSelection = true; + + emit changed(); +} + + +/// +/// Can we undo? +/// +bool UndoRedoModel::canUndo() const +{ + return !mUndoStack.isEmpty(); +} + + +/// +/// Can we redo? +/// +bool UndoRedoModel::canRedo() const +{ + return !mRedoStack.isEmpty(); +} + + +/// +/// Undo description +/// +QString UndoRedoModel::undoDescription() const +{ + if ( canUndo() ) + { + return mUndoStack.topState()->description; + } + else + { + return ""; + } +} + + +/// +/// Redo description +/// +QString UndoRedoModel::redoDescription() const +{ + if ( canRedo() ) + { + return mRedoStack.topState()->description; + } + else + { + return ""; + } +} + + +/// +/// Selection changed handler +/// +void UndoRedoModel::onSelectionChanged() +{ + mNewSelection = true; +} + + +/// +/// State constructor +/// +UndoRedoModel::State::State( LabelModel* model, const QString& description ) +{ + this->model = model->save(); + this->description = description; +} + + +/// +/// State destructor +/// +UndoRedoModel::State::~State() +{ + delete model; +} + + +/// +/// Stack constructor +/// +UndoRedoModel::Stack::Stack() +{ +} + + +/// +/// Stack destructor +/// +UndoRedoModel::Stack::~Stack() +{ + clear(); +} + + +/// +/// Push state onto stack +/// +void UndoRedoModel::Stack::push( UndoRedoModel::State* state ) +{ + list.push_front( state ); +} + + +/// +/// Pop state from stack +/// +UndoRedoModel::State* UndoRedoModel::Stack::pop() +{ + return list.takeFirst(); +} + + +/// +/// Peek at state at top of stack +/// +const UndoRedoModel::State* UndoRedoModel::Stack::topState() const +{ + return list.first(); +} + + +/// +/// Is stack empty? +/// +bool UndoRedoModel::Stack::isEmpty() const +{ + return list.isEmpty(); +} + + +/// +/// Clear stack +/// +void UndoRedoModel::Stack::clear() +{ + while ( !isEmpty() ) + { + delete pop(); + } +} diff --git a/glabels/UndoRedoModel.h b/glabels/UndoRedoModel.h new file mode 100644 index 0000000..975ce4e --- /dev/null +++ b/glabels/UndoRedoModel.h @@ -0,0 +1,119 @@ +/* UndoRedoModel.h + * + * Copyright (C) 2016 Jim Evins + * + * This file is part of gLabels-qt. + * + * gLabels-qt is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * gLabels-qt 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with gLabels-qt. If not, see . + */ + +#ifndef UndoRedoModel_h +#define UndoRedoModel_h + +#include +#include +#include + +class LabelModel; + + +/// +/// UndoRedoModel +/// +struct UndoRedoModel : QObject +{ + Q_OBJECT + + + ///////////////////////////////// + // Life Cycle + ///////////////////////////////// +public: + UndoRedoModel( LabelModel* model ); + virtual ~UndoRedoModel(); + + + ///////////////////////////////// + // Public Methods + ///////////////////////////////// +public: + void checkpoint( const QString& description ); + void undo(); + void redo(); + bool canUndo() const; + bool canRedo() const; + QString undoDescription() const; + QString redoDescription() const; + + + ///////////////////////////////// + // Slots + ///////////////////////////////// +private slots: + void onSelectionChanged(); + + + ///////////////////////////////// + // Signals + ///////////////////////////////// +signals: + void changed(); + + + ///////////////////////////////// + // Private types + ///////////////////////////////// +private: + class State + { + public: + State( LabelModel* model, const QString& description ); + ~State(); + + LabelModel* model; + QString description; + }; + + class Stack + { + public: + Stack(); + ~Stack(); + + void push( State* state ); + State* pop(); + const State* topState() const; + bool isEmpty() const; + void clear(); + + private: + QList list; + }; + + + ///////////////////////////////// + // Private data + ///////////////////////////////// +private: + LabelModel *mModel; + + Stack mUndoStack; + Stack mRedoStack; + + bool mNewSelection; + QString mLastDescription; + +}; + +#endif // UndoRedoModel_h