diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index fefc4a8d0..3cd57a3ae 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -30,9 +30,9 @@ add_subdirectory(render) add_subdirectory(task) add_subdirectory(tool) add_subdirectory(ui) +add_subdirectory(undo) add_subdirectory(widget) add_subdirectory(window) -add_subdirectory(undo) set(OLIVE_TARGET "olive-editor") if(APPLE) diff --git a/app/project/projectviewmodel.cpp b/app/project/projectviewmodel.cpp index 15d19795c..1fdab293d 100644 --- a/app/project/projectviewmodel.cpp +++ b/app/project/projectviewmodel.cpp @@ -23,6 +23,8 @@ #include #include +#include "undo/undostack.h" + ProjectViewModel::ProjectViewModel(QObject *parent) : QAbstractItemModel(parent), project_(nullptr) @@ -55,7 +57,7 @@ QModelIndex ProjectViewModel::index(int row, int column, const QModelIndex &pare } // Get the parent object (project root if the index is invalid) - Item* item_parent = (parent.isValid()) ? static_cast(parent.internalPointer()) : project_->root(); + Item* item_parent = GetItemObjectFromIndex(parent); // Return an index to this object return createIndex(row, column, item_parent->child(row)); @@ -64,7 +66,7 @@ QModelIndex ProjectViewModel::index(int row, int column, const QModelIndex &pare QModelIndex ProjectViewModel::parent(const QModelIndex &child) const { // Get the Item object from the index - Item* item = static_cast(child.internalPointer()); + Item* item = GetItemObjectFromIndex(child); // Get Item's parent object Item* par = item->parent(); @@ -75,7 +77,7 @@ QModelIndex ProjectViewModel::parent(const QModelIndex &child) const } // Otherwise return a true index to its parent - int parent_index = indexOfChild(par); + int parent_index = IndexOfChild(par); // Make sure the index is valid (there's no reason it shouldn't be) Q_ASSERT(parent_index > -1); @@ -97,7 +99,7 @@ int ProjectViewModel::rowCount(const QModelIndex &parent) const } // Otherwise, the index must contain a valid pointer, so we just return its child count - return static_cast(parent.internalPointer())->child_count(); + return GetItemObjectFromIndex(parent)->child_count(); } int ProjectViewModel::columnCount(const QModelIndex &parent) const @@ -114,7 +116,7 @@ int ProjectViewModel::columnCount(const QModelIndex &parent) const QVariant ProjectViewModel::data(const QModelIndex &index, int role) const { - Item* internal_item = static_cast(index.internalPointer()); + Item* internal_item = GetItemObjectFromIndex(index); switch (role) { case Qt::DisplayRole: @@ -172,7 +174,7 @@ bool ProjectViewModel::hasChildren(const QModelIndex &parent) const { // Check if this is a valid index if (parent.isValid()) { - Item* item = static_cast(parent.internalPointer()); + Item* item = GetItemObjectFromIndex(parent); // Check if this item is a kFolder type // If it's a folder, we always return TRUE in order to always show the "expand triangle" icon, @@ -213,7 +215,7 @@ QMimeData *ProjectViewModel::mimeData(const QModelIndexList &indexes) const if (index.isValid()) { // Check if we've dragged this item before if (!dragged_items.contains(index.internalPointer())) { - // If not, add it to the stream (and also keep track of it in the vector)s + // If not, add it to the stream (and also keep track of it in the vector) stream << index.row() << reinterpret_cast(index.internalPointer()); dragged_items.append(index.internalPointer()); } @@ -248,19 +250,11 @@ bool ProjectViewModel::dropMimeData(const QMimeData *data, Qt::DropAction action QDataStream stream(&model_data, QIODevice::ReadOnly); // Get the Item object that the items were dropped on - Item* drop_location; + Item* drop_location = GetItemObjectFromIndex(drop); - // If the index is valid, move them to the containing object - if (drop.isValid()) { - drop_location = static_cast(drop.internalPointer()); - - // If this is not a folder, we cannot drop these items here - if (drop_location->type() != Item::kFolder) { - return false; - } - } else { - // If the index isn't valid, the items are moving to the root - drop_location = project_->root(); + // If this is not a folder, we cannot drop these items here + if (drop_location->type() != Item::kFolder) { + return false; } // Variables to deserialize into @@ -268,47 +262,44 @@ bool ProjectViewModel::dropMimeData(const QMimeData *data, Qt::DropAction action int r; // Loop through all data + QUndoCommand* move_command = new QUndoCommand(); + while (!stream.atEnd()) { stream >> r >> item_ptr; - // Get the Item pointer from the mime data Item* item = reinterpret_cast(item_ptr); - // Get the Item's parent - Item* parent_item = item->parent(); + // Check if Item is already the drop location or if its parent is the drop location, in which case this is a + // no-op - // If the Drop Item is the Item or its parent already, this is a no-op - if (item != drop_location && parent_item != drop_location) { + if (item != drop_location && item->parent() != drop_location && !ItemIsParentOfChild(item, drop_location)) { + MoveItemCommand* mic = new MoveItemCommand(this, item, static_cast(drop_location), move_command); - // Get an index to the parent - QModelIndex parent_index; - - // If the parent item is not the root, we'll need to actually create an index - if (parent_item != project()->root()) { - parent_index = createIndex(indexOfChild(parent_item), 0, parent_item); - } - - // Signal to all views that we're about to move an object - beginMoveRows(parent_index, r, r, drop, drop_location->child_count()); - - // Move the object - item->set_parent(drop_location); - - endMoveRows(); + Q_UNUSED(mic) } } + if (move_command->childCount() > 0) { + olive::undo_stack.push(move_command); + } else { + delete move_command; + } + return true; } return false; } -int ProjectViewModel::indexOfChild(Item *item) const +int ProjectViewModel::IndexOfChild(Item *item) const { // Find parent's index within its own parent // (TODO: this model should handle sorting, which means it'll have to "know" the indices) + if (item == project_->root()) { + return -1; + } + Item* parent = item->parent(); if (parent != nullptr) { @@ -321,3 +312,75 @@ int ProjectViewModel::indexOfChild(Item *item) const return -1; } + +int ProjectViewModel::ChildCount(const QModelIndex &index) +{ + Item* item = GetItemObjectFromIndex(index); + + return item->child_count(); +} + +Item *ProjectViewModel::GetItemObjectFromIndex(const QModelIndex &index) const +{ + if (index.isValid()) { + return static_cast(index.internalPointer()); + } + + return project_->root(); +} + +bool ProjectViewModel::ItemIsParentOfChild(Item *parent, Item *child) const +{ + // Loop through parent hierarchy checking if `parent` is one of its parents + do { + child = child->parent(); + + if (parent == child) { + return true; + } + } while (child != nullptr); + + return false; +} + +void ProjectViewModel::MoveItemInternal(Item *item, Item *destination) +{ + QModelIndex item_index = CreateIndexFromItem(item); + + QModelIndex destination_index = CreateIndexFromItem(destination); + + beginMoveRows(item_index.parent(), item_index.row(), item_index.row(), destination_index, destination->child_count()); + + item->set_parent(destination); + + endMoveRows(); +} + +QModelIndex ProjectViewModel::CreateIndexFromItem(Item *item) +{ + return createIndex(IndexOfChild(item), 0, item); +} + +ProjectViewModel::MoveItemCommand::MoveItemCommand(ProjectViewModel *model, + Item *item, + Folder *destination, + QUndoCommand *parent) : + QUndoCommand(parent), + model_(model), + item_(item), + destination_(destination) +{ + source_ = static_cast(item->parent()); + + setText(tr("Move Item")); +} + +void ProjectViewModel::MoveItemCommand::redo() +{ + model_->MoveItemInternal(item_, destination_); +} + +void ProjectViewModel::MoveItemCommand::undo() +{ + model_->MoveItemInternal(item_, source_); +} diff --git a/app/project/projectviewmodel.h b/app/project/projectviewmodel.h index a565af06f..ca8dbb17f 100644 --- a/app/project/projectviewmodel.h +++ b/app/project/projectviewmodel.h @@ -22,6 +22,7 @@ #define VIEWMODEL_H #include +#include #include "project.h" @@ -89,11 +90,82 @@ public: virtual QMimeData * mimeData(const QModelIndexList &indexes) const override; virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; private: - int indexOfChild(Item* item) const; + /** + * @brief Retrieve the index of `item` in its parent + * + * This function will return the index of a specified item in its parent according to whichever sorting algorithm + * is currently active. + * + * @return + * + * Index of the specified item, or -1 if the item is root (in which case it has no parent). + */ + int IndexOfChild(Item* item) const; + + /** + * @brief Get the child count of an index + * + * @param index + * + * @return + * + * Return number of children (immediate children only) + */ + int ChildCount(const QModelIndex& index); + + /** + * @brief Retrieves the Item object from a given index + * + * A convenience function for retrieving Item objects. If the index is not valid, this returns the root Item. + */ + Item* GetItemObjectFromIndex(const QModelIndex& index) const; + + /** + * @brief Check if an Item is a parent of a Child + * + * Checks entire "parent hierarchy" of `child` to see if `parent` is one of its parents. + */ + bool ItemIsParentOfChild(Item* parent, Item* child) const; + + /** + * @brief Moves an item to a new destination updating all views in the process + * + * This function will emit a signal indicating that rows are moving, set `destination` as the new parent of `item`, + * and then emit a signal that the row has finished moving. + * + * It's not recommended to use this function directly in most cases since it does not create a QUndoCommand allowing + * the user to undo the move. Instead this function should primarily be called from QUndoCommands belonging to this + * class (e.g. MoveItemCommand). + */ + void MoveItemInternal(Item* item, Item* destination); + + /** + * @brief Convenience function for creating QModelIndexes from an Item object + */ + QModelIndex CreateIndexFromItem(Item* item); Project* project_; QVector columns_; + + /** + * @brief A QUndoCommand for moving an item from one folder to another folder + */ + class MoveItemCommand : public QUndoCommand { + public: + MoveItemCommand(ProjectViewModel* model, Item* item, Folder* destination, QUndoCommand* parent = nullptr); + + virtual void redo() override; + + virtual void undo() override; + + private: + ProjectViewModel* model_; + Item* item_; + Folder* source_; + Folder* destination_; + + }; }; #endif // VIEWMODEL_H diff --git a/app/ui/style/css/olive-dark.css b/app/ui/style/css/olive-dark.css index 469f43912..77c95c4d1 100644 --- a/app/ui/style/css/olive-dark.css +++ b/app/ui/style/css/olive-dark.css @@ -28,6 +28,7 @@ Main: #353535 Text: #ffffff + Disabled Text: #808080 Highlights: #2a82da Highlight Text: #ffffff Dark: #191919 @@ -46,6 +47,11 @@ QWidget::selected { color: #ffffff; } +/* Default disabled colors */ +QWidget::disabled { + color: #808080; +} + /* Default link color */ a { color: #2a82da; diff --git a/app/ui/style/css/olive-light.css b/app/ui/style/css/olive-light.css index 66d623a1b..711f075e2 100644 --- a/app/ui/style/css/olive-light.css +++ b/app/ui/style/css/olive-light.css @@ -28,6 +28,7 @@ Main: #d0d0d0 Text: #000000 + Disabled Text: #808080 Highlights: #2a82da Highlight Text: #ffffff Dark: #f0f0f0 @@ -46,6 +47,11 @@ QWidget::selected { color: #ffffff; } +/* Default disabled colors */ +QWidget::disabled { + color: #808080; +} + /* Default link color */ a { color: #2a82da; diff --git a/app/ui/style/css/olive-mid.css b/app/ui/style/css/olive-mid.css index 8340b2294..7829ec9e0 100644 --- a/app/ui/style/css/olive-mid.css +++ b/app/ui/style/css/olive-mid.css @@ -28,6 +28,7 @@ Main: #808080 Text: #000000 + Disabled Text: #808080 Highlights: #2a82da Highlight Text: #ffffff Dark: #c0c0c0 @@ -46,6 +47,11 @@ QWidget::selected { color: #ffffff; } +/* Default disabled colors */ +QWidget::disabled { + color: #808080; +} + /* Default link color */ a { color: #2a82da; diff --git a/app/widget/menu/menu.cpp b/app/widget/menu/menu.cpp index 9e8723570..9384db4f7 100644 --- a/app/widget/menu/menu.cpp +++ b/app/widget/menu/menu.cpp @@ -62,6 +62,17 @@ QAction *Menu::CreateItem(QObject* parent, { QAction* a = new QAction(parent); + ConformItem(a, + id, + receiver, + member, + key); + + return a; +} + +void Menu::ConformItem(QAction* a, const QString &id, const QObject *receiver, const char *member, const QString &key) +{ a->setProperty("id", id); if (!key.isEmpty()) { @@ -69,9 +80,9 @@ QAction *Menu::CreateItem(QObject* parent, a->setProperty("keydefault", key); } - connect(a, SIGNAL(triggered(bool)), receiver, member); - - return a; + if (receiver != nullptr) { + connect(a, SIGNAL(triggered(bool)), receiver, member); + } } void Menu::SetBooleanAction(QAction *a, bool* boolean) diff --git a/app/widget/menu/menu.h b/app/widget/menu/menu.h index a6c6514fa..a956fb20f 100644 --- a/app/widget/menu/menu.h +++ b/app/widget/menu/menu.h @@ -41,6 +41,11 @@ public: const char* member, const QString& key = QString()); + static void ConformItem(QAction *a, const QString& id, + const QObject* receiver, + const char* member, + const QString& key = QString()); + static void SetBooleanAction(QAction* a, bool *boolean); private: diff --git a/app/window/mainwindow/mainmenu.cpp b/app/window/mainwindow/mainmenu.cpp index 49887e30e..837a9a9d0 100644 --- a/app/window/mainwindow/mainmenu.cpp +++ b/app/window/mainwindow/mainmenu.cpp @@ -25,6 +25,7 @@ #include "core.h" #include "tool/tool.h" #include "ui/style/style.h" +#include "undo/undostack.h" #include "widget/menu/menushared.h" MainMenu::MainMenu(QWidget *parent) : @@ -52,8 +53,14 @@ MainMenu::MainMenu(QWidget *parent) : // EDIT MENU // edit_menu_ = new Menu(this); - edit_undo_item_ = edit_menu_->AddItem("undo", nullptr, nullptr, "Ctrl+Z"); - edit_redo_item_ = edit_menu_->AddItem("redo", nullptr, nullptr, "Ctrl+Shift+Z"); + + edit_undo_item_ = olive::undo_stack.createUndoAction(this); //edit_menu_->AddItem("undo", nullptr, nullptr, "Ctrl+Z"); + Menu::ConformItem(edit_undo_item_, "undo", nullptr, nullptr, "Ctrl+Z"); + edit_menu_->addAction(edit_undo_item_); + edit_redo_item_ = olive::undo_stack.createRedoAction(this); //edit_menu_->AddItem("redo", nullptr, nullptr, "Ctrl+Shift+Z"); + Menu::ConformItem(edit_redo_item_, "redo", nullptr, nullptr, "Ctrl+Shift+Z"); + edit_menu_->addAction(edit_redo_item_); + edit_menu_->addSeparator(); olive::menu_shared.AddItemsForEditMenu(edit_menu_); edit_menu_->addSeparator(); @@ -325,8 +332,8 @@ void MainMenu::Retranslate() // Edit menu edit_menu_->setTitle(tr("&Edit")); - edit_undo_item_->setText(tr("&Undo")); - edit_redo_item_->setText(tr("Redo")); + //edit_undo_item_->setText(tr("&Undo")); FIXME: Does Qt translate these automatically? + //edit_redo_item_->setText(tr("Redo")); edit_select_all_item_->setText(tr("Select &All")); edit_deselect_all_item_->setText(tr("Deselect All")); edit_ripple_to_in_item_->setText(tr("Ripple to In Point"));