1 /*
2  * LibrePCB - Professional EDA for everyone!
3  * Copyright (C) 2013 LibrePCB Developers, see AUTHORS.md for contributors.
4  * https://librepcb.org/
5  *
6  * This program is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19 
20 #ifndef LIBREPCB_UNDOSTACK_H
21 #define LIBREPCB_UNDOSTACK_H
22 
23 /*******************************************************************************
24  *  Includes
25  ******************************************************************************/
26 #include "exceptions.h"
27 
28 #include <QtCore>
29 
30 /*******************************************************************************
31  *  Namespace / Forward Declarations
32  ******************************************************************************/
33 namespace librepcb {
34 
35 class UndoStack;
36 class UndoCommand;
37 class UndoCommandGroup;
38 
39 /*******************************************************************************
40  *  Class UndoStackTransaction
41  ******************************************************************************/
42 
43 /**
44  * @brief The UndoStackTransaction class helps to execute transactions on an
45  * UndoStack
46  *
47  * This class allows to use RAII on a librepcb::UndoStack object to make its
48  * exception safety easier. The functionality is as follows:
49  * @li The ctor starts a new command group with
50  * librepcb::UndoStack::beginCmdGroup().
51  * @li If necessary, the dtor aborts it with
52  * librepcb::UndoStack::abortCmdGroup().
53  * @li #append() redirects to librepcb::UndoStack::appendToCmdGroup().
54  * @li #commit() redirects to librepcb::UndoStack::commitCmdGroup().
55  * @li #abort() redirects to librepcb::UndoStack::abortCmdGroup().
56  */
57 class UndoStackTransaction final {
58 public:
59   // Constructors / Destructor
60   UndoStackTransaction() = delete;
61   UndoStackTransaction(const UndoStackTransaction& other) = delete;
62   UndoStackTransaction(UndoStack& stack, const QString& text);
63   ~UndoStackTransaction() noexcept;
64 
65   // General Methods
66   void append(UndoCommand* cmd);
67   void abort();
68   void commit();
69 
70   // Operator Overloadings
71   UndoStackTransaction& operator=(const UndoStackTransaction& rhs) = delete;
72 
73 private:
74   UndoStack& mStack;
75   bool mCmdActive;
76 };
77 
78 /*******************************************************************************
79  *  Class UndoStack
80  ******************************************************************************/
81 
82 /**
83  * @brief The UndoStack class holds UndoCommand objects and provides undo/redo
84  * commands
85  *
86  * Instead of the Qt classes QUndoStack and QUndoCommand we use our own undo
87  * classes ::librepcb::UndoStack and ::librepcb::UndoCommand because of the
88  * better exception handling and more flexibility.
89  *
90  * @note Our classes work very similar to the equivalent classes of Qt, so
91  * please read the documentation of "Qt's Undo Framework" and classes QUndoStack
92  * and QUndoCommand. There is also a more detailed description here: @ref
93  * doc_project_undostack
94  *
95  * Compared with QUndoStack, the biggest differences are the following:
96  *  - <b>Support for exceptions:</b> If an exception is thrown in an
97  * ::librepcb::UndoCommand object, this undo stack always tries to keep the
98  * whole stack consistent (update the index only if the last undo/redo was
99  * successful, try to rollback failed changes, ...).
100  *  - <b>Removed support for nested macros (QUndoStack#beginMacro() and
101  *    QUndoStack#endMacro())</b>: I think we do need this feature (but we have a
102  * similar mechanism, see next line)...
103  *  - <b>Added support for exclusive macro command creation:</b>
104  *
105  * @see ::librepcb::UndoCommand, ::librepcb::UndoCommandGroup
106  */
107 class UndoStack final : public QObject {
108   Q_OBJECT
109 
110 public:
111   // Constructors / Destructor
112   UndoStack(const UndoStack& other) = delete;
113   UndoStack& operator=(const UndoStack& rhs) = delete;
114 
115   /**
116    * @brief The default constructor
117    */
118   UndoStack() noexcept;
119 
120   /**
121    * @brief The destructor (will also call #clear())
122    */
123   ~UndoStack() noexcept;
124 
125   // Getters
126 
127   /**
128    * @brief Get the text for the undo action
129    * @return The text in the user's language ("Undo" if undo is not possible)
130    */
131   QString getUndoText() const noexcept;
132 
133   /**
134    * @brief Get the text for the redo action
135    * @return The text in the user's language ("Redo" if redo is not possible)
136    */
137   QString getRedoText() const noexcept;
138 
139   /**
140    * @brief Check if undo is possible
141    * @return true | false
142    */
143   bool canUndo() const noexcept;
144 
145   /**
146    * @brief Check if redo is possible
147    * @return true | false
148    */
149   bool canRedo() const noexcept;
150 
151   /**
152    * @brief Get a unique identification of the current state
153    *
154    * Useful to detect if there were any changes made between to different
155    * points in time.
156    *
157    * @return The current state identification
158    */
159   uint getUniqueStateId() const noexcept;
160 
161   /**
162    * @brief Check if the stack is in a clean state (the state of the last
163    * #setClean())
164    *
165    * This is used to determine if the document/project/whatever has changed
166    * since the last time it was saved. You need to call #setClean() when you
167    * save it.
168    *
169    * @return true | false
170    */
171   bool isClean() const noexcept;
172 
173   /**
174    * @brief Check if a command group is active at the moment (see
175    * #mActiveCommandGroup)
176    *
177    * @return True if a command group is currently active
178    */
179   bool isCommandGroupActive() const noexcept;
180 
181   // Setters
182 
183   /**
184    * @brief Set the current state as the clean state (see also #isClean())
185    */
186   void setClean() noexcept;
187 
188   // General Methods
189 
190   /**
191    * @brief Execute a command and push it to the stack (similar to
192    * QUndoStack#push())
193    *
194    * @param cmd       The command to execute (must NOT be executed already). The
195    *                  stack will ALWAYS take the ownership over this command,
196    * even if this method throws an exception because of an error. In case of an
197    * exception, the command will be deleted directly in this method, so you must
198    * not make other things with the UndoCommand object after passing it to this
199    * method.
200    * @param forceKeepCmd  Only for internal use!
201    *
202    * @retval true     If the command has done some changes
203    * @retval false    If the command has done nothing
204    *
205    * @throw Exception If the command is not executed successfully, this method
206    *                  throws an exception and tries to keep the state of the
207    * stack consistent (as the passed command did never exist).
208    *
209    * @note If you try to execute a command with that method while another
210    * command is active (see #isCommandGroupActive()), this method will throw an
211    * exception.
212    */
213   bool execCmd(UndoCommand* cmd, bool forceKeepCmd = false);
214 
215   /**
216    * @brief Begin building a new command group that consists of multiple
217    * commands step by step (over a "long" time)
218    *
219    * @param text      The text of the whole command group (see
220    * UndoCommand#getText())
221    *
222    * @throw Exception This method throws an exception if there is already
223    * another command group active (#isCommandGroupActive()) or if an error
224    *                  occurs.
225    */
226   void beginCmdGroup(const QString& text);
227 
228   /**
229    * @brief Append a new command to the currently active command group
230    *
231    * This method must only be called between #beginCmdGroup() and
232    * #commitCmdGroup() or #abortCmdGroup().
233    *
234    * @param cmd       The command to execute (same conditions as for
235    * #execCmd()!)
236    *
237    * @retval true     If the command has done some changes
238    * @retval false    If the command has done nothing
239    *
240    * @throw Exception This method throws an exception if there is no command
241    * group active at the moment (#isCommandGroupActive()) or if an error occurs.
242    */
243   bool appendToCmdGroup(UndoCommand* cmd);
244 
245   /**
246    * @brief End the currently active command group and keep the changes
247    *
248    * @retval true     If the command group has done some changes
249    * @retval false    If the command group has done nothing
250    *
251    * @throw Exception This method throws an exception if there is no command
252    * group active at the moment (#isCommandGroupActive()) or if an error occurs.
253    */
254   bool commitCmdGroup();
255 
256   /**
257    * @brief End the currently active command group and revert the changes
258    *
259    * @throw Exception This method throws an exception if there is no command
260    * group active at the moment (#isCommandGroupActive()) or if an error occurs.
261    */
262   void abortCmdGroup();
263 
264   /**
265    * @brief Undo the last command
266    *
267    * @note If you call this method while another command group is currently
268    * active
269    *       (#isCommandGroupActive()), this method will do nothing.
270    *
271    * @throw Exception If an error occurs, this class tries to revert all changes
272    *                  to restore the state of BEFORE calling this method. But
273    * there is no guarantee that this will work correctly...
274    */
275   void undo();
276 
277   /**
278    * @brief Redo the last undoed command
279    *
280    * @throw Exception If an error occurs, this class tries to revert all changes
281    *                  to restore the state of BEFORE calling this method. But
282    * there is no guarantee that this will work correctly...
283    */
284   void redo();
285 
286   /**
287    * @brief Clear the whole stack (delete all UndoCommand objects)
288    *
289    * All UndoCommand objects will be deleted in the reverse order of their
290    * creation (the newest first, the oldest at last).
291    */
292   void clear() noexcept;
293 
294 signals:
295   void undoTextChanged(const QString& text);
296   void redoTextChanged(const QString& text);
297   void canUndoChanged(bool canUndo);
298   void canRedoChanged(bool canRedo);
299   void cleanChanged(bool clean);
300   void commandGroupEnded();
301   void commandGroupAborted();
302   void stateModified();
303 
304 private:
305   /**
306    * @brief This list holds all commands of the undo stack
307    *
308    * The first (oldest) command is at index zero (bottom of the stack), the last
309    * (newest) command is at index "count-1" (top of the stack).
310    */
311   QList<UndoCommand*> mCommands;
312 
313   /**
314    * @brief This attribute holds the current position in the undo stack
315    * #mCommands
316    *
317    * The value of this variable points to the index which the NEXT pushed
318    * command will have in the list #mCommands. So if the list is empty, this
319    * variable has the value zero.
320    */
321   int mCurrentIndex;
322 
323   /**
324    * @brief The index of the command list where the stack was cleaned the last
325    * time
326    */
327   int mCleanIndex;
328 
329   /**
330    * @brief If a command group is active at the moment, this is the pointer to
331    * it
332    *
333    * This pointer is only valid between calls to #beginCmdGroup() and
334    * #commitCmdGroup() or #abortCmdGroup(). Otherwise, the variable contains the
335    * nullptr.
336    */
337   UndoCommandGroup* mActiveCommandGroup;
338 };
339 
340 /*******************************************************************************
341  *  End of File
342  ******************************************************************************/
343 
344 }  // namespace librepcb
345 
346 #endif  // LIBREPCB_UNDOSTACK_H
347