From 83dea1b3b1b23e5d596e7dffc32dcf14aedb09e5 Mon Sep 17 00:00:00 2001 From: Mark Liversedge Date: Thu, 31 Dec 2015 12:01:21 +0000 Subject: [PATCH] Workout Editor Redo/Undo .. we only have two commands; create and move point but baking this in early so we can adopt it for all other commands as they arrive. .. due to the interactive nature of a graphical editor the command class behaves differently to the one used on the ride data editor; commands are added to the stack when they complete (so move point isn't a history of the mouse cursor moving its just the begin and end point). --- src/WorkoutWidget.cpp | 93 ++++++++++++++++++++++++++++++++++++-- src/WorkoutWidget.h | 40 +++++++++++++++- src/WorkoutWidgetItems.cpp | 54 ++++++++++++++++++++++ src/WorkoutWidgetItems.h | 33 ++++++++++++++ src/WorkoutWindow.cpp | 14 +----- src/WorkoutWindow.h | 8 ++-- 6 files changed, 219 insertions(+), 23 deletions(-) diff --git a/src/WorkoutWidget.cpp b/src/WorkoutWidget.cpp index 2d22cb68b..e7ee2021b 100644 --- a/src/WorkoutWidget.cpp +++ b/src/WorkoutWidget.cpp @@ -48,7 +48,7 @@ static const int SPACING = 2; // between labels and tics (if there are tics) static bool GRIDLINES = true; WorkoutWidget::WorkoutWidget(WorkoutWindow *parent, Context *context) : - QWidget(parent), ergFile(NULL), state(none), dragging(NULL), parent(parent), context(context) + QWidget(parent), ergFile(NULL), state(none), dragging(NULL), parent(parent), context(context), stackptr(0) { maxX_=3600; maxY_=300; @@ -73,8 +73,6 @@ WorkoutWidget::timeout() bool WorkoutWidget::eventFilter(QObject *obj, QEvent *event) { - // where were we when we did Create ? - static QPoint onCreate; // process as normal if not one of ours if (obj != this) return false; @@ -165,6 +163,7 @@ WorkoutWidget::eventFilter(QObject *obj, QEvent *event) if (point->bounding().contains(p)) { updateNeeded=true; dragging = point; + onDrag = QPointF(dragging->x, dragging->y); state = drag; break; } @@ -189,6 +188,15 @@ WorkoutWidget::eventFilter(QObject *obj, QEvent *event) // 3. MOUSE RELEASED // if (event->type() == QEvent::MouseButtonRelease) { + + if (state == drag && dragging) { + + // create command to reflect the drag, but only + // if it actually moved! + if (dragging->x != onDrag.x() || dragging->y != onDrag.y()) + new MovePointCommand(this, onDrag, QPointF(dragging->x, dragging->y), points_.indexOf(dragging)); + } + state = none; dragging = NULL; updateNeeded = true; @@ -291,25 +299,35 @@ WorkoutWidget::createPoint(QPoint p) dragging = new WWPoint(this, to.x(), to.y(), false); state = drag; + onDrag = QPointF(dragging->x, dragging->y); // yuk, this should be done in the FSM (eventFilter) + // action at a distance ... yuk XXX TIDY THIS XXX // add into the points for(int i=0; ix > to.x()) { points_.insert(i, dragging); + new CreatePointCommand(this, to.x(), to.y(), i); return true; } } // after current points_.append(dragging); + new CreatePointCommand(this, to.x(), to.y(), -1); return true; } void WorkoutWidget::ergFileSelected(ErgFile *ergFile) { - // reset state + // reset state and stack state = none; dragging = NULL; + foreach (WorkoutWidgetCommand *p, stack) delete p; + stack.clear(); + stackptr = 0; + parent->undoAct->setEnabled(false); + parent->redoAct->setEnabled(false); + //XXX consider refactoring this !!! XXX // wipe out points foreach(WWPoint *point, points_) delete point; @@ -549,3 +567,70 @@ WorkoutWidget::reverseTransform(int x, int y) return QPoint((x-c.x()) / xratio, (c.bottomLeft().y() - y) / yratio); } + +WorkoutWidgetCommand::WorkoutWidgetCommand(WorkoutWidget *w) : workoutWidget_(w) +{ + // add us + w->addCommand(this); +} + +// add to the stack, don't execute since it was already executed +// and this is a memento to enable undo / redo +void +WorkoutWidget::addCommand(WorkoutWidgetCommand *cmd) +{ + // stop dragging + dragging = NULL; + state = none; + + // truncate if needed + if (stack.count()) { + // wipe away commands we can no longer redo + while (stack.count() > stackptr) { + WorkoutWidgetCommand *p = stack.takeAt(stackptr); + delete p; + } + } + + // add to stack + stack.append(cmd); + stackptr++; + + // set undo enabled, redo disabled + parent->undoAct->setEnabled(true); + parent->redoAct->setEnabled(false); +} + +void +WorkoutWidget::redo() +{ + // stop dragging + dragging = NULL; + state = none; + + // redo if we can + if (stackptr >= 0 && stackptr < stack.count()) stack[stackptr++]->redo(); + + // disable/enable buttons + if (stackptr > 0) parent->undoAct->setEnabled(true); + if (stackptr >= stack.count()) parent->redoAct->setEnabled(false); + + update(); +} + +void +WorkoutWidget::undo() +{ + // stop dragging + dragging = NULL; + state = none; + + // run it + if (stackptr > 0) stack[--stackptr]->undo(); + + // disable/enable button + if (stackptr <= 0) parent->undoAct->setEnabled(false); + if (stackptr < stack.count()) parent->redoAct->setEnabled(true); + + update(); +} diff --git a/src/WorkoutWidget.h b/src/WorkoutWidget.h index 91efde308..7ac3f97ff 100644 --- a/src/WorkoutWidget.h +++ b/src/WorkoutWidget.h @@ -29,6 +29,7 @@ #include #include #include +#include class ErgFile; class WorkoutWindow; @@ -59,6 +60,25 @@ class WorkoutWidgetItem { WorkoutWidget *w; }; +// base for all commands that operate on a workout (e.g. create +// point, drag point, delete etc etc used by redo/undo +class WorkoutWidgetCommand +{ + public: + + WorkoutWidgetCommand(WorkoutWidget *w); // will add to stack + virtual ~WorkoutWidgetCommand() {} + + WorkoutWidget *workoutWidget() { return workoutWidget_; } + + // need to reimplement these in subclass + virtual void undo() = 0; + virtual void redo() = 0; + + private: + WorkoutWidget *workoutWidget_; +}; + class WorkoutWidget : public QWidget { Q_OBJECT @@ -72,10 +92,10 @@ class WorkoutWidget : public QWidget void addPoint(WWPoint*x) { points_.append(x); } // get list of my items - QList children() { return children_; } + QList &children() { return children_; } // get list of my items - QList points() { return points_; } + QList &points() { return points_; } // range for scales in plot units not draw units double maxX(); // e.g. max watts @@ -116,6 +136,14 @@ class WorkoutWidget : public QWidget // timeout on click timer void timeout(); + // if done mid stack, the stack is truncated + void addCommand(WorkoutWidgetCommand *cmd); + + // redo / undo command by going + // up and down the stack + void redo(); + void undo(); + protected: // interaction state @@ -141,6 +169,14 @@ class WorkoutWidget : public QWidget QList points_; double maxX_, maxY_; + + // command stack + QList stack; + int stackptr; + + // where were we when we changed state? + QPoint onCreate; + QPointF onDrag; }; #endif // _GC_WorkoutWidget_h diff --git a/src/WorkoutWidgetItems.cpp b/src/WorkoutWidgetItems.cpp index 9ff8933af..f29bb1580 100644 --- a/src/WorkoutWidgetItems.cpp +++ b/src/WorkoutWidgetItems.cpp @@ -180,3 +180,57 @@ WWLine::paint(QPainter *painter) painter->fillPath(path, QBrush(linearGradient)); } } + +// +// COMMANDS +// + +// Create a new point +CreatePointCommand::CreatePointCommand(WorkoutWidget *w, double x, double y, int index) + : WorkoutWidgetCommand(w), x(x), y(y), index(index) { } + +// undo create point +void +CreatePointCommand::undo() +{ + // wipe it from the array + WWPoint *p = NULL; + if (index >= 0) p = workoutWidget()->points().takeAt(index); + else p = workoutWidget()->points().takeAt(workoutWidget()->points().count()-1); + delete p; +} + +// do it again +void +CreatePointCommand::redo() +{ + // create a new WWPoint + WWPoint *p = new WWPoint(workoutWidget(), x,y, false); + + // -1 means append + if (index < 0) + workoutWidget()->points().append(p); + else + workoutWidget()->points().insert(index, p); +} + +MovePointCommand::MovePointCommand(WorkoutWidget *w, QPointF before, QPointF after, int index) + : WorkoutWidgetCommand(w), before(before), after(after), index(index) { } + +void +MovePointCommand::undo() +{ + WWPoint *p = workoutWidget()->points()[index]; + + p->x = before.x(); + p->y = before.y(); +} + +void +MovePointCommand::redo() +{ + WWPoint *p = workoutWidget()->points()[index]; + + p->x = after.x(); + p->y = after.y(); +} diff --git a/src/WorkoutWidgetItems.h b/src/WorkoutWidgetItems.h index d3bbb16bd..576e32e02 100644 --- a/src/WorkoutWidgetItems.h +++ b/src/WorkoutWidgetItems.h @@ -36,6 +36,9 @@ #define GCWW_LINE 3 #define GCWW_WSCALE 4 //W'bal +// +// ITEMS +// class WWPowerScale : public WorkoutWidgetItem { public: @@ -117,5 +120,35 @@ class WWLine : public WorkoutWidgetItem { }; +// +// COMMANDS +// +class CreatePointCommand : public WorkoutWidgetCommand +{ + public: + CreatePointCommand(WorkoutWidget *w, double x, double y, int index); + void redo(); + void undo(); + + private: + + //state info + double x,y; + int index; +}; + +class MovePointCommand : public WorkoutWidgetCommand +{ + public: + MovePointCommand(WorkoutWidget *w, QPointF before, QPointF after, int index); + void redo(); + void undo(); + + private: + + //state info + QPointF before, after; + int index; +}; #endif // _GC_WorkoutWidgetItems_h diff --git a/src/WorkoutWindow.cpp b/src/WorkoutWindow.cpp index acfdec741..5d068e193 100644 --- a/src/WorkoutWindow.cpp +++ b/src/WorkoutWindow.cpp @@ -64,12 +64,12 @@ WorkoutWindow::WorkoutWindow(Context *context) : // icon in that instance would be horrible QIcon undoIcon(":images/toolbar/undo.png"); undoAct = new QAction(undoIcon, tr("Undo"), this); - connect(undoAct, SIGNAL(triggered()), this, SLOT(undo())); + connect(undoAct, SIGNAL(triggered()), workout, SLOT(undo())); toolbar->addAction(undoAct); QIcon redoIcon(":images/toolbar/redo.png"); redoAct = new QAction(redoIcon, tr("Redo"), this); - connect(redoAct, SIGNAL(triggered()), this, SLOT(redo())); + connect(redoAct, SIGNAL(triggered()), workout, SLOT(redo())); toolbar->addAction(redoAct); toolbar->addSeparator(); @@ -135,16 +135,6 @@ WorkoutWindow::saveFile() { } -void -WorkoutWindow::undo() -{ -} - -void -WorkoutWindow::redo() -{ -} - void WorkoutWindow::drawMode() { diff --git a/src/WorkoutWindow.h b/src/WorkoutWindow.h index de85b8a53..843dd08ac 100644 --- a/src/WorkoutWindow.h +++ b/src/WorkoutWindow.h @@ -47,14 +47,15 @@ class WorkoutWindow : public GcWindow WorkoutWindow(Context *context); + // workout widget updates these QLabel *xlabel, *ylabel; + QAction *saveAct, *undoAct, *redoAct, + *drawAct, *selectAct; public slots: // toolbar functions void saveFile(); - void undo(); - void redo(); void drawMode(); void selectMode(); @@ -66,9 +67,6 @@ class WorkoutWindow : public GcWindow Context *context; QToolBar *toolbar; - QAction *saveAct, *undoAct, *redoAct, - *drawAct, *selectAct; - WorkoutWidget *workout; // will become editor. WWPowerScale *powerscale; WWLine *line;