From 459ad13edad256c1fe58bb1460e247aba23a8ac7 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 21 Jun 2019 11:51:50 -0700 Subject: [PATCH] wrote viewmodel model for project browsing --- app/CMakeLists.txt | 5 +- app/core.cpp | 13 ++ app/core.h | 24 +++ app/panel/project/project.cpp | 14 +- app/panel/project/project.h | 6 + app/project/CMakeLists.txt | 6 + app/project/folder.cpp | 11 ++ app/project/folder.h | 17 +++ app/project/item.cpp | 64 ++++++++ app/project/item.h | 42 +++++ app/project/project.cpp | 4 + app/project/project.h | 10 ++ app/project/projectviewmodel.cpp | 143 ++++++++++++++++++ app/project/projectviewmodel.h | 65 ++++++++ .../projectexplorer/projectexplorer.cpp | 14 +- app/widget/projectexplorer/projectexplorer.h | 12 +- app/window/mainwindow/mainmenu.h | 16 ++ app/window/mainwindow/mainwindow.cpp | 28 ++-- app/window/mainwindow/mainwindow.h | 8 + 19 files changed, 486 insertions(+), 16 deletions(-) create mode 100644 app/project/folder.cpp create mode 100644 app/project/folder.h create mode 100644 app/project/item.cpp create mode 100644 app/project/item.h create mode 100644 app/project/projectviewmodel.cpp create mode 100644 app/project/projectviewmodel.h diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 4f5ca956a..5fc19e482 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -47,7 +47,10 @@ add_executable(${OLIVE_TARGET} target_compile_definitions(${OLIVE_TARGET} PRIVATE ${OLIVE_DEFINITIONS}) -target_compile_options(${OLIVE_TARGET} PRIVATE $<$:-Werror -pedantic-errors -Wall -Wextra -Wconversion -Wsign-conversion>) +target_compile_options( + ${OLIVE_TARGET} + PRIVATE + $<$:-Werror -pedantic-errors -Wall -Wextra -Wconversion -Wsign-conversion>) target_link_libraries(${OLIVE_TARGET} PRIVATE diff --git a/app/core.cpp b/app/core.cpp index 21c2de974..375d02f17 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -85,6 +85,13 @@ void Core::Start() } else { main_window_->showMaximized(); } + + // When a new project is opened, update the mainwindow + connect(this, SIGNAL(ProjectOpened(Project*)), main_window_, SLOT(ProjectOpen(Project*))); + + // Create new project on startup + // TODO: Load project from startup_project_ instead if not empty + AddOpenProject(std::make_shared()); } void Core::Stop() @@ -96,3 +103,9 @@ olive::MainWindow *Core::main_window() { return main_window_; } + +void Core::AddOpenProject(ProjectPtr p) +{ + open_projects_.append(p); + emit ProjectOpened(p.get()); +} diff --git a/app/core.h b/app/core.h index 21e8cc4ac..6bb09bbd0 100644 --- a/app/core.h +++ b/app/core.h @@ -21,6 +21,9 @@ #ifndef CORE_H #define CORE_H +#include + +#include "project/project.h" #include "window/mainwindow/mainwindow.h" /** @@ -62,7 +65,23 @@ public: * Pointer to the olive::MainWindow object, or nullptr if running in CLI mode. */ olive::MainWindow* main_window(); + +signals: + /** + * @brief Signal emitted when a project is opened + * + * Connects to main window so its UI can update based on the project + * + * @param p + */ + void ProjectOpened(Project* p); + private: + /** + * @brief Creates an empty project and adds it to the "open projects" + */ + void AddOpenProject(ProjectPtr p); + /** * @brief Internal main window object */ @@ -75,6 +94,11 @@ private: * project URL here to be loaded once Olive has finished initializing. */ QString startup_project_; + + /** + * @brief List of currently open projects + */ + QList open_projects_; }; namespace olive { diff --git a/app/panel/project/project.cpp b/app/panel/project/project.cpp index a423485d8..9451c7937 100644 --- a/app/panel/project/project.cpp +++ b/app/panel/project/project.cpp @@ -19,13 +19,23 @@ ProjectPanel::ProjectPanel(QWidget *parent) : layout->addWidget(toolbar); // Set up main explorer object - ProjectExplorer* explorer = new ProjectExplorer(this); - layout->addWidget(explorer); + explorer_ = new ProjectExplorer(this); + layout->addWidget(explorer_); // Set strings Retranslate(); } +Project *ProjectPanel::project() +{ + return explorer_->project(); +} + +void ProjectPanel::set_project(Project *p) +{ + explorer_->set_project(p); +} + void ProjectPanel::changeEvent(QEvent *e) { if (e->type() == QEvent::LanguageChange) { diff --git a/app/panel/project/project.h b/app/panel/project/project.h index bbcf975c9..ed802b194 100644 --- a/app/panel/project/project.h +++ b/app/panel/project/project.h @@ -1,6 +1,7 @@ #ifndef PROJECT_PANEL_H #define PROJECT_PANEL_H +#include "project/project.h" #include "widget/panel/panel.h" #include "widget/projectexplorer/projectexplorer.h" @@ -10,11 +11,16 @@ class ProjectPanel : public PanelWidget public: ProjectPanel(QWidget* parent); + Project* project(); + void set_project(Project* p); + protected: virtual void changeEvent(QEvent* e) override; private: void Retranslate(); + + ProjectExplorer* explorer_; }; #endif // PROJECT_H diff --git a/app/project/CMakeLists.txt b/app/project/CMakeLists.txt index 421066ffe..bceeb1898 100644 --- a/app/project/CMakeLists.txt +++ b/app/project/CMakeLists.txt @@ -16,7 +16,13 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} + project/folder.h + project/folder.cpp + project/item.h + project/item.cpp project/project.h project/project.cpp + project/projectviewmodel.h + project/projectviewmodel.cpp PARENT_SCOPE ) diff --git a/app/project/folder.cpp b/app/project/folder.cpp new file mode 100644 index 000000000..ca82c7555 --- /dev/null +++ b/app/project/folder.cpp @@ -0,0 +1,11 @@ +#include "folder.h" + +Folder::Folder() +{ + +} + +Item::Type Folder::type() const +{ + return kFolder; +} diff --git a/app/project/folder.h b/app/project/folder.h new file mode 100644 index 000000000..24daf2da1 --- /dev/null +++ b/app/project/folder.h @@ -0,0 +1,17 @@ +#ifndef FOLDER_H +#define FOLDER_H + +#include "item.h" + +class Folder : public Item +{ +public: + Folder(); + + virtual Type type() const override; + +private: + +}; + +#endif // FOLDER_H diff --git a/app/project/item.cpp b/app/project/item.cpp new file mode 100644 index 000000000..4814b0f95 --- /dev/null +++ b/app/project/item.cpp @@ -0,0 +1,64 @@ +#include "item.h" + +Item::Item() : + parent_(nullptr) +{ +} + +Item::~Item() +{ +} + +void Item::add_child(Item* c) +{ + if (c->parent_ == this) { + return; + } + + children_.append(c); + c->parent_ = this; +} + +void Item::remove_child(Item *c) +{ + if (c->parent_ != this) { + return; + } + + children_.removeAll(c); + c->parent_ = nullptr; +} + +int Item::child_count() +{ + return children_.size(); +} + +Item *Item::child(int i) +{ + return children_.at(i); +} + +const QString &Item::name() const +{ + return name_; +} + +void Item::set_name(const QString &n) +{ + name_ = n; +} + +Item *Item::parent() const +{ + return parent_; +} + +void Item::set_parent(Item *p) +{ + if (parent_ != nullptr) { + parent_->remove_child(this); + } + + p->add_child(this); +} diff --git a/app/project/item.h b/app/project/item.h new file mode 100644 index 000000000..c51beeab6 --- /dev/null +++ b/app/project/item.h @@ -0,0 +1,42 @@ +#ifndef ITEM_H +#define ITEM_H + +#include +#include + +class Item +{ +public: + enum Type { + kFolder, + kFootage, + kSequence + }; + + Item(); + + // Required virtual destructor (it's empty) + virtual ~Item(); + + virtual Type type() const = 0; + + void add_child(Item *c); + void remove_child(Item* c); + int child_count(); + Item* child(int i); + + const QString& name() const; + void set_name(const QString& n); + + Item *parent() const; + void set_parent(Item *p); + +private: + QList children_; + + Item* parent_; + + QString name_; +}; + +#endif // ITEM_H diff --git a/app/project/project.cpp b/app/project/project.cpp index bb8dccb02..f1117c14b 100644 --- a/app/project/project.cpp +++ b/app/project/project.cpp @@ -2,5 +2,9 @@ Project::Project() { +} +Folder *Project::root() +{ + return &root_; } diff --git a/app/project/project.h b/app/project/project.h index a28a2d676..af360e1cc 100644 --- a/app/project/project.h +++ b/app/project/project.h @@ -1,11 +1,21 @@ #ifndef PROJECT_H #define PROJECT_H +#include + +#include "folder.h" class Project { public: Project(); + + Folder* root(); + +private: + Folder root_; }; +using ProjectPtr = std::shared_ptr; + #endif // PROJECT_H diff --git a/app/project/projectviewmodel.cpp b/app/project/projectviewmodel.cpp new file mode 100644 index 000000000..d9094f972 --- /dev/null +++ b/app/project/projectviewmodel.cpp @@ -0,0 +1,143 @@ +#include "projectviewmodel.h" + +ProjectViewModel::ProjectViewModel(QObject *parent) : + QAbstractItemModel(parent), + project_(nullptr) +{ + // TODO make this configurable + columns_.append(kName); + columns_.append(kDuration); + columns_.append(kRate); +} + +Project *ProjectViewModel::project() +{ + return project_; +} + +void ProjectViewModel::set_project(Project *p) +{ + beginResetModel(); + + project_ = p; + + endResetModel(); +} + +QModelIndex ProjectViewModel::index(int row, int column, const QModelIndex &parent) const +{ + // I'm actually not 100% sure what this does, but it seems logical and was in the earlier code + if (!hasIndex(row, column, parent)) { + return QModelIndex(); + } + + // Get the parent object (project root if the index is invalid) + Item* item_parent = (parent.isValid()) ? static_cast(parent.internalPointer()) : project_->root(); + + // Return an index to this object + return createIndex(row, column, item_parent->child(row)); +} + +QModelIndex ProjectViewModel::parent(const QModelIndex &child) const +{ + // Get the Item object from the index + Item* item = static_cast(child.internalPointer()); + + // Get Item's parent object + Item* parent = item->parent(); + + // If the parent is the root, return an empty index + if (parent == project_->root()) { + return QModelIndex(); + } + + // Otherwise return a true index to its parent + + // Find parent's index within its own parent + // (TODO: this model should handle sorting, which means it'll have to "know" the indices) + int child_index = -1; + + Item* double_parent = parent->parent(); + for (int i=0;ichild_count();i++) { + if (double_parent->child(i) == parent) { + child_index = i; + break; + } + } + + // Make sure the index is valid (there's no reason it shouldn't be) + Q_ASSERT(child_index > -1); + + // Return an index to the parent + return createIndex(child_index, 0, parent); +} + +int ProjectViewModel::rowCount(const QModelIndex &parent) const +{ + // If there's no project, there are obviously no items to show + if (project_ == nullptr) { + return 0; + } + + // If the index is the root, return the root child count + if (parent == QModelIndex()) { + return project_->root()->child_count(); + } + + // Otherwise, the index must contain a valid pointer, so we just return its child count + return static_cast(parent.internalPointer())->child_count(); +} + +int ProjectViewModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + + // Not strictly necessary, but a decent visual cue that there's no project currently active + if (project_ == nullptr) { + return 0; + } + + return columns_.size(); +} + +QVariant ProjectViewModel::data(const QModelIndex &index, int role) const +{ + if (role == Qt::DisplayRole) { + + ColumnType column_type = columns_.at(index.column()); + + switch (column_type) { + case kName: + return static_cast(index.internalPointer())->name(); + case kDuration: + return "00:00:00;00"; + case kRate: + return "29.97 FPS"; + } + } + + // TODO Add DecorationRole to column 1 for icons + + return QVariant(); +} + +QVariant ProjectViewModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + // Check if we need text data (DisplayRole) and orientation is horizontal + // FIXME I'm not 100% sure what happens if the orientation is vertical/if that check is necessary + if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { + ColumnType column_type = columns_.at(section); + + // Return the name based on the column's current type + switch (column_type) { + case kName: + return tr("Name"); + case kDuration: + return tr("Duration"); + case kRate: + return tr("Rate"); + } + } + + return QAbstractItemModel::headerData(section, orientation, role); +} diff --git a/app/project/projectviewmodel.h b/app/project/projectviewmodel.h new file mode 100644 index 000000000..5bfb4223e --- /dev/null +++ b/app/project/projectviewmodel.h @@ -0,0 +1,65 @@ +#ifndef VIEWMODEL_H +#define VIEWMODEL_H + +#include + +#include "project.h" + +/** + * @brief The ProjectViewModel class + * + * An adapter that interprets the data in a Project into a Qt item model for usage in ViewModel Views. + */ +class ProjectViewModel : public QAbstractItemModel +{ +public: + enum ColumnType { + /// Media name + kName, + + /// Media duration + kDuration, + + /// Media rate (frame rate for video, sample rate for audio) + kRate + }; + + ProjectViewModel(QObject* parent); + + /** + * @brief Get currently active project + * + * @return + * + * Currently active project or nullptr if there is none + */ + Project* project(); + + /** + * @brief Set the project to adapt + * + * Any views attached to this model will get updated by this function. + * + * @param p + * + * Project to adapt, can be set to nullptr to "close" the project (will show an empty model that cannot be modified) + */ + void set_project(Project* p); + + /** Compulsory Qt QAbstractItemModel overrides */ + virtual QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + virtual QModelIndex parent(const QModelIndex &child) const override; + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override; + virtual int columnCount(const QModelIndex &parent = QModelIndex()) const override; + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + /** Optional Qt QAbstractItemModel overrides */ + virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + +private: + Project* project_; + + QVector columns_; +}; + +#endif // VIEWMODEL_H diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index 2db91fdc2..77423b81c 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -2,9 +2,11 @@ ProjectExplorer::ProjectExplorer(QWidget *parent) : QStackedWidget(parent), - view_type_(TreeView) + view_type_(TreeView), + model_(this) { tree_view_ = new QTreeView(this); + tree_view_->setModel(&model_); addWidget(tree_view_); } @@ -17,3 +19,13 @@ void ProjectExplorer::set_view_type(const ProjectExplorer::ViewType &type) { view_type_ = type; } + +Project *ProjectExplorer::project() +{ + return model_.project(); +} + +void ProjectExplorer::set_project(Project *p) +{ + model_.set_project(p); +} diff --git a/app/widget/projectexplorer/projectexplorer.h b/app/widget/projectexplorer/projectexplorer.h index ef1f7f5ac..6a8475acb 100644 --- a/app/widget/projectexplorer/projectexplorer.h +++ b/app/widget/projectexplorer/projectexplorer.h @@ -1,9 +1,12 @@ -#ifndef PROJECTEXPLORER_H +#ifndef PROJECTEXPLORER_H #define PROJECTEXPLORER_H #include #include +#include "project/project.h" +#include "project/projectviewmodel.h" + class ProjectExplorer : public QStackedWidget { public: @@ -18,10 +21,15 @@ public: const ViewType& view_type(); void set_view_type(const ViewType& type); + Project* project(); + void set_project(Project* p); + private: + QTreeView* tree_view_; + ViewType view_type_; - QTreeView* tree_view_; + ProjectViewModel model_; }; #endif // PROJECTEXPLORER_H diff --git a/app/window/mainwindow/mainmenu.h b/app/window/mainwindow/mainmenu.h index e8e7058cb..826852f00 100644 --- a/app/window/mainwindow/mainmenu.h +++ b/app/window/mainwindow/mainmenu.h @@ -25,15 +25,31 @@ #include "widget/menu/menu.h" +/** + * @brief The MainMenu class + * + * Olive's menubar attached to its main window. Responsible for creating the menu, connecting signals/slots, and + * retranslating the items on a language change. + */ class MainMenu : public QMenuBar { public: MainMenu(QWidget* parent); protected: + /** + * @brief changeEvent + * + * Qt changeEvent override to catch a QEvent::LanguageEvent. + * + * @param e + */ virtual void changeEvent(QEvent* e); private: + /** + * @brief Set strings based on the current application language. + */ void Retranslate(); Menu* file_menu_; diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 3d10c3526..919f08eec 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -43,16 +43,24 @@ olive::MainWindow::MainWindow(QWidget *parent) : // Allow panels to be tabbed within each other setDockNestingEnabled(true); - // TODO Use settings data to create panels and restore state if they exist - ProjectPanel* viewer_panel = new ProjectPanel(this); - addDockWidget(Qt::TopDockWidgetArea, viewer_panel); - ViewerPanel* viewer_panel2 = new ViewerPanel(this); - addDockWidget(Qt::TopDockWidgetArea, viewer_panel2); - ViewerPanel* viewer_panel4 = new ViewerPanel(this); - addDockWidget(Qt::TopDockWidgetArea, viewer_panel4); - TimelinePanel* viewer_panel3 = new TimelinePanel(this); - addDockWidget(Qt::BottomDockWidgetArea, viewer_panel3); - + // Create and set main menu MainMenu* main_menu = new MainMenu(this); setMenuBar(main_menu); } + +void olive::MainWindow::ProjectOpen(Project* p) +{ + // TODO Use settings data to create panels and restore state if they exist + ProjectPanel* project_panel = new ProjectPanel(this); + project_panel->set_project(p); + addDockWidget(Qt::TopDockWidgetArea, project_panel); + + ViewerPanel* viewer_panel1 = new ViewerPanel(this); + addDockWidget(Qt::TopDockWidgetArea, viewer_panel1); + + ViewerPanel* viewer_panel2 = new ViewerPanel(this); + addDockWidget(Qt::TopDockWidgetArea, viewer_panel2); + + TimelinePanel* timeline_panel = new TimelinePanel(this); + addDockWidget(Qt::BottomDockWidgetArea, timeline_panel); +} diff --git a/app/window/mainwindow/mainwindow.h b/app/window/mainwindow/mainwindow.h index d5dd0213c..7da7f140e 100644 --- a/app/window/mainwindow/mainwindow.h +++ b/app/window/mainwindow/mainwindow.h @@ -23,6 +23,8 @@ #include +#include "project/project.h" + namespace olive { /** @@ -34,7 +36,13 @@ class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget* parent = nullptr); + +public slots: + void ProjectOpen(Project *p); + private: + + }; }