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