From c19718a4a560fbabfed8a6b882d940f184dee8eb Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 25 Sep 2019 02:33:33 +1000 Subject: [PATCH] reimplemented goto prev/next cut, about dialog, and action search dialog --- app/core.cpp | 7 + app/core.h | 5 + app/dialog/CMakeLists.txt | 2 + app/dialog/about/CMakeLists.txt | 22 + app/dialog/about/about.cpp | 65 ++ app/dialog/about/about.h | 48 + app/dialog/actionsearch/CMakeLists.txt | 22 + app/dialog/actionsearch/actionsearch.cpp | 253 +++++ app/dialog/actionsearch/actionsearch.h | 181 ++++ app/dialog/preferences/preferences.cpp | 1197 ++++++++++++++++++++++ app/dialog/preferences/preferences.h | 432 ++++++++ app/panel/panelmanager.cpp | 5 + app/panel/panelmanager.h | 12 + app/panel/timeline/timeline.cpp | 10 + app/panel/timeline/timeline.h | 4 + app/panel/viewer/viewer.cpp | 15 + app/panel/viewer/viewer.h | 6 + app/undo/undostack.cpp | 20 + app/undo/undostack.h | 20 + app/widget/panel/panel.h | 10 + app/widget/timelineview/timelineview.cpp | 72 +- app/widget/timelineview/timelineview.h | 6 + app/widget/viewer/viewer.cpp | 79 +- app/widget/viewer/viewer.h | 10 + app/window/mainwindow/mainmenu.cpp | 49 +- app/window/mainwindow/mainmenu.h | 11 + app/window/mainwindow/mainwindow.cpp | 31 + app/window/mainwindow/mainwindow.h | 3 +- 28 files changed, 2573 insertions(+), 24 deletions(-) create mode 100644 app/dialog/about/CMakeLists.txt create mode 100644 app/dialog/about/about.cpp create mode 100644 app/dialog/about/about.h create mode 100644 app/dialog/actionsearch/CMakeLists.txt create mode 100644 app/dialog/actionsearch/actionsearch.cpp create mode 100644 app/dialog/actionsearch/actionsearch.h create mode 100644 app/dialog/preferences/preferences.cpp create mode 100644 app/dialog/preferences/preferences.h diff --git a/app/core.cpp b/app/core.cpp index a668bec2d..b4b3c0415 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -28,6 +28,7 @@ #include #include +#include "dialog/about/about.h" #include "dialog/sequence/sequence.h" #include "panel/panelmanager.h" #include "panel/project/project.h" @@ -160,6 +161,12 @@ void Core::SetSnapping(const bool &b) emit SnappingChanged(snapping_); } +void Core::DialogAboutShow() +{ + AboutDialog a(main_window_); + a.exec(); +} + void Core::DialogImportShow() { // Open dialog for user to select files diff --git a/app/core.h b/app/core.h index 388a6197f..769c557a7 100644 --- a/app/core.h +++ b/app/core.h @@ -114,6 +114,11 @@ public slots: */ void SetSnapping(const bool& b); + /** + * @brief Show an About dialog + */ + void DialogAboutShow(); + /** * @brief Open the import footage dialog and import the files selected (runs ImportFiles()) */ diff --git a/app/dialog/CMakeLists.txt b/app/dialog/CMakeLists.txt index 8d32061fe..010e47f7a 100644 --- a/app/dialog/CMakeLists.txt +++ b/app/dialog/CMakeLists.txt @@ -14,6 +14,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +add_subdirectory(about) +add_subdirectory(actionsearch) add_subdirectory(sequence) set(OLIVE_SOURCES diff --git a/app/dialog/about/CMakeLists.txt b/app/dialog/about/CMakeLists.txt new file mode 100644 index 000000000..c4a932e77 --- /dev/null +++ b/app/dialog/about/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2019 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + dialog/about/about.h + dialog/about/about.cpp + PARENT_SCOPE +) diff --git a/app/dialog/about/about.cpp b/app/dialog/about/about.cpp new file mode 100644 index 000000000..76c6fd941 --- /dev/null +++ b/app/dialog/about/about.cpp @@ -0,0 +1,65 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "about.h" + +#include +#include +#include +#include + +AboutDialog::AboutDialog(QWidget *parent) : + QDialog(parent) +{ + setWindowTitle(tr("About %1").arg(QApplication::applicationName())); + + QVBoxLayout* layout = new QVBoxLayout(this); + //layout->setSpacing(20); + + // Construct About text + QLabel* label = + new QLabel(QString("" + "

" + "

" + "" + "https://www.olivevideoeditor.org/" + "

" + "

%1

" // AppName (version identifier) + "

%2

" // First statement + "

%3

" // Second statement + "").arg(QApplication::applicationName(), + tr("Olive is a non-linear video editor. This software is free and " + "protected by the GNU GPL."), + tr("Olive Team is obliged to inform users that Olive source code is " + "available for download from its website.")),this); + + // Set text formatting + label->setAlignment(Qt::AlignCenter); + label->setTextInteractionFlags(Qt::TextSelectableByMouse); + label->setCursor(Qt::IBeamCursor); + label->setWordWrap(true); + layout->addWidget(label); + + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok, this); + buttons->setCenterButtons(true); + layout->addWidget(buttons); + + connect(buttons, SIGNAL(accepted()), this, SLOT(accept())); +} diff --git a/app/dialog/about/about.h b/app/dialog/about/about.h new file mode 100644 index 000000000..1c4839b56 --- /dev/null +++ b/app/dialog/about/about.h @@ -0,0 +1,48 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef ABOUTDIALOG_H +#define ABOUTDIALOG_H + +#include + +/** + * @brief The AboutDialog class + * + * The About dialog (accessible through Help > About). Contains license and version information. This can be run from + * anywhere + */ +class AboutDialog : public QDialog +{ + Q_OBJECT +public: + /** + * @brief AboutDialog Constructor + * + * Creates About dialog. + * + * @param parent + * + * QWidget parent object. Usually this will be MainWindow. + */ + explicit AboutDialog(QWidget *parent = nullptr); +}; + +#endif // ABOUTDIALOG_H diff --git a/app/dialog/actionsearch/CMakeLists.txt b/app/dialog/actionsearch/CMakeLists.txt new file mode 100644 index 000000000..16d7e7a68 --- /dev/null +++ b/app/dialog/actionsearch/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2019 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + dialog/actionsearch/actionsearch.h + dialog/actionsearch/actionsearch.cpp + PARENT_SCOPE +) diff --git a/app/dialog/actionsearch/actionsearch.cpp b/app/dialog/actionsearch/actionsearch.cpp new file mode 100644 index 000000000..b5227655a --- /dev/null +++ b/app/dialog/actionsearch/actionsearch.cpp @@ -0,0 +1,253 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "actionsearch.h" + +#include +#include +#include +#include + +ActionSearch::ActionSearch(QWidget *parent) : + QDialog(parent), + menu_bar_(nullptr) +{ + // ActionSearch requires a parent widget + Q_ASSERT(parent != nullptr); + + // Set styling (object name is required for CSS specific to this object) + setObjectName("ASDiag"); + setStyleSheet("#ASDiag{border: 2px solid #808080;}"); + + // Size proportionally to the parent (usually MainWindow). + resize(parent->width()/3, parent->height()/3); + + // Show dialog as a "popup", which will make the dialog close if the user clicks out of it. + setWindowFlags(Qt::Popup); + + QVBoxLayout* layout = new QVBoxLayout(this); + + // Construct the main entry text field. + ActionSearchEntry* entry_field = new ActionSearchEntry(this); + + // Set the main entry field font size to 1.2x its standard font size. + QFont entry_field_font = entry_field->font(); + entry_field_font.setPointSize(qRound(entry_field_font.pointSize()*1.2)); + entry_field->setFont(entry_field_font); + + // Set placeholder text for the main entry field + entry_field->setPlaceholderText(tr("Search for action...")); + + // Connect signals/slots + connect(entry_field, SIGNAL(textChanged(const QString&)), this, SLOT(search_update(const QString &))); + connect(entry_field, SIGNAL(returnPressed()), this, SLOT(perform_action())); + + // moveSelectionUp() and moveSelectionDown() are emitted when the user pressed up or down on the text field. + // We override it here to select the upper or lower item in the list. + connect(entry_field, SIGNAL(moveSelectionUp()), this, SLOT(move_selection_up())); + connect(entry_field, SIGNAL(moveSelectionDown()), this, SLOT(move_selection_down())); + layout->addWidget(entry_field); + + // Construct list of actions + list_widget = new ActionSearchList(this); + + // Set list's font to 1.2x its standard font size + QFont list_widget_font = list_widget->font(); + list_widget_font.setPointSize(qRound(list_widget_font.pointSize()*1.2)); + list_widget->setFont(list_widget_font); + + layout->addWidget(list_widget); + + connect(list_widget, SIGNAL(dbl_click()), this, SLOT(perform_action())); + + // Instantly focus on the entry field to allow for fully keyboard operation (if this popup was initiated by keyboard + // shortcut for example). + entry_field->setFocus(); +} + +void ActionSearch::SetMenuBar(QMenuBar *menu_bar) +{ + menu_bar_ = menu_bar; +} + +void ActionSearch::search_update(const QString &s, const QString &p, QMenu *parent) +{ + // Do nothing if there's no menu bar to work with + if (menu_bar_ == nullptr) { + return; + } + + // This function is recursive, using the `parent` parameter to loop through a menu's items. It functions in two + // modes - the parent being NULL, meaning it'll get MainWindow's menubar and loop over its menus, and the parent + // referring to a menu at which point it'll loop over its actions (and call itself recursively if it finds any + // submenus). + + if (parent == nullptr) { + + // If parent is NULL, we'll pull from the MainWindow's menubar and call this recursively on all of its submenus + // (and their submenus). + + // We'll clear all the current items in the list since if we're here, we're just starting. + list_widget->clear(); + + QList menus = menu_bar_->actions(); + + // Loop through all menus from the menubar and run this function on each one. + for (int i=0;imenu(); + + search_update(s, p, menu); + } + + // Once we're here, all the recursion/item retrieval is complete. We auto-select the first item for better + // keyboard-exclusive functionality. + if (list_widget->count() > 0) { + list_widget->item(0)->setSelected(true); + } + + } else { + + // Parent was not NULL, so we loop over the actions in the menu we were given in `parent`. + + // The list shows a '>' delimited hierarchy of the menus in which this action came from. We construct it here by + // adding the current menu's text to the existing hierarchy (passed in `p`). + QString menu_text; + if (!p.isEmpty()) menu_text += p + " > "; + menu_text += parent->title().replace("&", ""); // Strip out any &s used in menu action names + + // Loop over the menu's actions + QList actions = parent->actions(); + for (int i=0;iisSeparator()) { + + if (a->menu() != nullptr) { + + // If the action is a menu, run this function recursively on it + search_update(s, menu_text, a->menu()); + + } else { + + // This is a valid non-separator non-menu action, so check it against the currently entered string. + + // Strip out all &s from the action's name + QString comp = a->text().replace("&", ""); + + // See if the action's name contains any of the currently entered string + if (comp.contains(s, Qt::CaseInsensitive)) { + + // If so, we add it to the list widget. + QListWidgetItem* item = new QListWidgetItem(QString("%1\n(%2)").arg(comp, menu_text), list_widget); + + // Add a pointer to the original QAction in the item's data + item->setData(Qt::UserRole+1, reinterpret_cast(a)); + + list_widget->addItem(item); + + } + + } + } + } + } +} + +void ActionSearch::perform_action() { + + // Loop over all the items in the list and if we find one that's selected, we trigger it. + QList selected_items = list_widget->selectedItems(); + if (list_widget->count() > 0 && selected_items.size() > 0) { + + QListWidgetItem* item = selected_items.at(0); + + // Get QAction pointer from item's data + QAction* a = reinterpret_cast(item->data(Qt::UserRole+1).value()); + + a->trigger(); + + } + + // Close this popup + accept(); + +} + +void ActionSearch::move_selection_up() { + + // Here we loop over all the items to find the currently selected one, and then select the one above it. We start + // iterating at 1 (instead of 0) to efficiently ignore the first item (since the selection can't go below the very + // bottom item). + + int lim = list_widget->count(); + for (int i=1;iitem(i)->isSelected()) { + list_widget->item(i-1)->setSelected(true); + list_widget->scrollToItem(list_widget->item(i-1)); + break; + } + } +} + +void ActionSearch::move_selection_down() { + + // Here we loop over all the items to find the currently selected one, and then select the one below it. We limit it + // one entry before count() to efficiently ignore the item at the end (since the selection can't go below the very + // bottom item). + + int lim = list_widget->count()-1; + for (int i=0;iitem(i)->isSelected()) { + list_widget->item(i+1)->setSelected(true); + list_widget->scrollToItem(list_widget->item(i+1)); + break; + } + } +} + +ActionSearchEntry::ActionSearchEntry(QWidget *parent) : QLineEdit(parent) {} + +void ActionSearchEntry::keyPressEvent(QKeyEvent * event) { + + // Listen for up/down, otherwise pass the key event to the base class. + + switch (event->key()) { + case Qt::Key_Up: + emit moveSelectionUp(); + break; + case Qt::Key_Down: + emit moveSelectionDown(); + break; + default: + QLineEdit::keyPressEvent(event); + } + +} + +ActionSearchList::ActionSearchList(QWidget *parent) : QListWidget(parent) {} + +void ActionSearchList::mouseDoubleClickEvent(QMouseEvent *) { + + // Indiscriminately emit a signal on any double click + emit dbl_click(); + +} diff --git a/app/dialog/actionsearch/actionsearch.h b/app/dialog/actionsearch/actionsearch.h new file mode 100644 index 000000000..ff48717e3 --- /dev/null +++ b/app/dialog/actionsearch/actionsearch.h @@ -0,0 +1,181 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef ACTIONSEARCH_H +#define ACTIONSEARCH_H + +#include +#include +#include +#include +#include + +class ActionSearchList; + +/** + * @brief The ActionSearch class + * + * A popup window (accessible through Help > Action Search) that allows users to search for a menu command by typing + * rather than browsing through the menu bar. This can be created from anywhere provided olive::MainWindow is valid. + */ +class ActionSearch : public QDialog +{ + Q_OBJECT +public: + /** + * @brief ActionSearch Constructor + * + * Create ActionSearch popup. + * + * @param parent + * + * QWidget parent. Usually MainWindow. + */ + ActionSearch(QWidget* parent); + + /** + * @brief Set the menu bar to use in this action search + */ + void SetMenuBar(QMenuBar* menu_bar); +private slots: + /** + * @brief Update the list of actions according to a search query + * + * This function adds/removes actions in the action list according to a given search query entered by the user. + * + * To loop over the menubar and all of its menus and submenus, this function will call itself recursively. As such + * some of its parameters do not need to be set externally, as these will be set by the function itself as it calls + * itself. + * + * @param s + * + * The search text. This is the only parameter that should be set externally. + * + * @param p + * + * The current parent hierarchy. In most cases, this should be left as nullptr when called externally. + * search_update() will fill this automatically as it needs while calling itself recursively. + * + * @param parent + * + * The current menu to loop over. In most cases, this should be left as nullptr when called externally. + * search_update() will fill this automatically as it needs while calling itself recursively. + */ + void search_update(const QString& s, const QString &p = nullptr, QMenu *parent = nullptr); + + /** + * @brief Perform the currently selected action + * + * Usually triggered by pressing Enter on the ActionSearchEntry field, this will trigger whatever action is currently + * highlighted and then close this popup. If no entries are highlighted (i.e. the list is empty), no action is + * triggered and the popup closes anyway. + */ + void perform_action(); + + /** + * @brief Move selection up + * + * A slot for pressing up on the ActionSearchEntry field. Moves the selection in the list up once. If the + * selection is already at the top of the list, this is a no-op. + */ + void move_selection_up(); + + /** + * @brief Move selection down + * + * A slot for pressing down on the ActionSearchEntry field. Moves the selection in the list down once. If the + * selection is already at the bottom of the list, this is a no-op. + */ + void move_selection_down(); +private: + /** + * @brief Main widget that shows the list of commands + */ + ActionSearchList* list_widget; + + /** + * @brief Attached menu bar object + */ + QMenuBar* menu_bar_; +}; + +/** + * @brief The ActionSearchList class + * + * Simple wrapper around QListWidget that emits a signal when an item is double clicked that ActionSearch connects + * to a slot that triggers the currently selected action. + */ +class ActionSearchList : public QListWidget { + Q_OBJECT +public: + /** + * @brief ActionSearchList Constructor + * @param parent + * + * Usually ActionSearch. + */ + ActionSearchList(QWidget* parent); +protected: + /** + * @brief Override of QListWidget's double click event that emits a signal. + */ + void mouseDoubleClickEvent(QMouseEvent *); +signals: + /** + * @brief Signal emitted when a QListWidget item is double clicked. + */ + void dbl_click(); +}; + +/** + * @brief The ActionSearchEntry class + * + * Simple wrapper around QLineEdit that emits signals when the up or down arrow keys are pressed so that ActionSearch + * can connect them to moving the current selection up or down. + */ +class ActionSearchEntry : public QLineEdit { + Q_OBJECT +public: + /** + * @brief ActionSearchEntry + * @param parent + * + * Usually ActionSearch. + */ + ActionSearchEntry(QWidget* parent); +protected: + /** + * @brief Override of QLineEdit's key press event that listens for up/down key presses. + * @param event + */ + void keyPressEvent(QKeyEvent * event); +signals: + /** + * @brief Emitted when the user presses the up arrow key. + */ + void moveSelectionUp(); + + /** + * @brief Emitted when the user presses the down arrow key. + */ + void moveSelectionDown(); +}; + +#endif // ACTIONSEARCH_H diff --git a/app/dialog/preferences/preferences.cpp b/app/dialog/preferences/preferences.cpp new file mode 100644 index 000000000..81eacefb3 --- /dev/null +++ b/app/dialog/preferences/preferences.cpp @@ -0,0 +1,1197 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "preferencesdialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "global/global.h" +#include "global/config.h" +#include "global/path.h" +#include "rendering/audio.h" +#include "rendering/pixelformats.h" +#include "panels/panels.h" +#include "ui/columnedgridlayout.h" +#include "ui/mainwindow.h" +#include "dialogs/newsequencedialog.h" + +KeySequenceEditor::KeySequenceEditor(QWidget* parent, QAction* a) + : QKeySequenceEdit(parent), action(a) { + setKeySequence(action->shortcut()); +} + +void KeySequenceEditor::set_action_shortcut() { + action->setShortcut(keySequence()); +} + +void KeySequenceEditor::reset_to_default() { + setKeySequence(action->property("default").toString()); +} + +QString KeySequenceEditor::action_name() { + return action->property("id").toString(); +} + +QString KeySequenceEditor::export_shortcut() { + QString ks = keySequence().toString(); + if (ks != action->property("default")) { + return action->property("id").toString() + "\t" + ks; + } + return nullptr; +} + +PreferencesDialog::PreferencesDialog(QWidget *parent) : + QDialog(parent) +{ + setWindowTitle(tr("Preferences")); + + setup_ui(); + + setup_kbd_shortcuts(olive::MainWindow->menuBar()); + + // set up default sequence + default_sequence.set_name(tr("Default Sequence")); + default_sequence.set_width(olive::config.default_sequence_width); + default_sequence.set_height(olive::config.default_sequence_height); + default_sequence.set_frame_rate(olive::config.default_sequence_framerate); + default_sequence.set_audio_frequency(olive::config.default_sequence_audio_frequency); + default_sequence.set_audio_layout(olive::config.default_sequence_audio_channel_layout); +} + +void PreferencesDialog::setup_kbd_shortcut_worker(QMenu* menu, QTreeWidgetItem* parent) { + QList actions = menu->actions(); + for (int i=0;iisSeparator() && a->property("keyignore").isNull()) { + QTreeWidgetItem* item = new QTreeWidgetItem(parent); + item->setText(0, a->text().replace("&", "")); + + parent->addChild(item); + + if (a->menu() != nullptr) { + item->setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator); + setup_kbd_shortcut_worker(a->menu(), item); + } else { + key_shortcut_items.append(item); + key_shortcut_actions.append(a); + } + } + } +} + +void PreferencesDialog::delete_previews(PreviewDeleteTypes type) { + char delete_char = 0; + + switch (type) { + case DELETE_WAVEFORMS: + delete_char = 'w'; + break; + case DELETE_THUMBNAILS: + delete_char = 't'; + break; + case DELETE_BOTH: + delete_char = 1; + break; + case DELETE_NONE: + break; + } + + if (delete_char != 't' && delete_char != 'w' && delete_char != 1) return; + + QDir preview_path(get_data_path() + "/previews"); + + if (delete_char == 1) { + // indiscriminately delete everything + preview_path.removeRecursively(); + } else { + QStringList preview_file_list = preview_path.entryList(QDir::Files | QDir::NoDotAndDotDot); + for (int i=0;i= 0 + && preview_file_str.at(identifier_char_index) >= 48 + && preview_file_str.at(identifier_char_index) <= 57) { + identifier_char_index--; + } + + // thumbnails will have a 't' towards the end of the filenames, waveforms will have a 'w' + // if they match the type of preview we're deleting, remove them + if (preview_file_str.at(identifier_char_index) == delete_char) { + QFile::remove(preview_path.filePath(preview_file_str)); + } + } + } +} + +void PreferencesDialog::populate_ocio_menus(OCIO::ConstConfigRcPtr config) +{ + if (!config) { + + // Just clear everything + ocio_display->clear(); + ocio_default_input->clear(); + ocio_view->clear(); + ocio_look->clear(); + + } else { + + // Get input color spaces for setting the default input color space + ocio_default_input->clear(); + for (int i=0;igetNumColorSpaces();i++) { + QString colorspace = config->getColorSpaceNameByIndex(i); + + ocio_default_input->addItem(colorspace); + + if (colorspace == olive::config.ocio_default_input_colorspace) { + ocio_default_input->setCurrentIndex(i); + } + } + + // Get current display name (if the config is empty, get the current default display) + QString current_display = olive::config.ocio_display; + if (current_display.isEmpty()) { + current_display = config->getDefaultDisplay(); + } + + // Populate the display menu + ocio_display->clear(); + for (int i=0;igetNumDisplays();i++) { + ocio_display->addItem(config->getDisplay(i)); + + // Check if this index is the currently selected + if (config->getDisplay(i) == current_display) { + ocio_display->setCurrentIndex(i); + } + } + + update_ocio_view_menu(config); + + // Populate the look menu + ocio_look->clear(); + ocio_look->addItem(tr("(None)"), QString()); + for (int i=0;igetNumLooks();i++) { + const char* look = config->getLookNameByIndex(i); + + ocio_look->addItem(look, look); + + if (look == olive::config.ocio_look) { + ocio_look->setCurrentIndex(i+1); + } + } + + } +} + +OCIO::ConstConfigRcPtr PreferencesDialog::TestOCIOConfig(const QString &url) +{ + // Check whether OCIO can load it + OCIO::ConstConfigRcPtr config; + try { + config = OCIO::Config::CreateFromFile(url.toUtf8()); + } catch (OCIO::Exception& e) { + QMessageBox::critical(this, + tr("OpenColorIO Config Error"), + tr("Failed to set OpenColorIO configuration: %1").arg(e.what()), + QMessageBox::Ok); + } + return config; +} + +void PreferencesDialog::update_ocio_view_menu(OCIO::ConstConfigRcPtr config) +{ + + // Get views for the current display set in `ocio_display` + QString display = ocio_display->currentText(); + + // Get current view + QString current_view = olive::config.ocio_view; + if (current_view.isEmpty()) { + current_view = config->getDefaultView(display.toUtf8()); + } + + // Populate the view menu + int ocio_view_count = config->getNumViews(display.toUtf8()); + ocio_view->clear(); + for (int i=0;igetView(display.toUtf8(), i); + + ocio_view->addItem(view); + + if (current_view == view) { + ocio_view->setCurrentIndex(i); + } + } +} + +void PreferencesDialog::update_ocio_config(const QString &s) +{ + OCIO::ConstConfigRcPtr file_config; + + if (!s.isEmpty() && QFileInfo::exists(s)) { + file_config = TestOCIOConfig(s); + } + + populate_ocio_menus(file_config); +} + +void PreferencesDialog::AddBoolPair(QCheckBox *ui, bool *value, bool restart_required) +{ + bool_ui.append(ui); + bool_value.append(value); + bool_restart_required.append(restart_required); + + ui->setChecked(*value); +} + +void PreferencesDialog::setup_kbd_shortcuts(QMenuBar* menubar) { + QList menus = menubar->actions(); + + for (int i=0;imenu(); + + QTreeWidgetItem* item = new QTreeWidgetItem(keyboard_tree); + item->setText(0, menu->title().replace("&", "")); + + keyboard_tree->addTopLevelItem(item); + + setup_kbd_shortcut_worker(menu, item); + } + + for (int i=0;iproperty("id").isNull()) { + KeySequenceEditor* editor = new KeySequenceEditor(keyboard_tree, key_shortcut_actions.at(i)); + keyboard_tree->setItemWidget(key_shortcut_items.at(i), 1, editor); + key_shortcut_fields.append(editor); + } + } +} + +void PreferencesDialog::accept() { + bool restart_after_saving = false; + bool reinit_audio = false; + bool reload_language = false; + bool reload_effects = false; + bool reset_ocio_shaders = false; + bool reset_render_threads = false; + + // Validate whether the specified CSS file exists + if (!custom_css_fn->text().isEmpty() && !QFileInfo::exists(custom_css_fn->text())) { + QMessageBox::critical( + this, + tr("Invalid CSS File"), + tr("CSS file '%1' does not exist.").arg(custom_css_fn->text()) + ); + return; + } + + // Validate whether the chosen OCIO configuration file + if (enable_color_management->isChecked()) { + + // Check whether the file exists + if (!QFileInfo::exists(ocio_config_file->text())) { + + QString msg_title = tr("Invalid OpenColorIO Configuration File"); + QString msg_body; + + if (ocio_config_file->text().isEmpty()) { + msg_body = tr("You must specify an OpenColorIO configuration file if color management is enabled."); + } else { + msg_body = tr("OpenColorIO configuration file '%1' does not exist.").arg(ocio_config_file->text()); + } + + QMessageBox::critical( + this, + msg_title, + msg_body + ); + return; + + } else if (olive::config.ocio_config_path != ocio_config_file->text()) { + + // Check whether OCIO can load it + OCIO::ConstConfigRcPtr file_config = TestOCIOConfig(ocio_config_file->text().toUtf8()); + + if (!file_config) { + return; + } + + } + } + + // Validate whether one of the bool options requires a restart + bool bool_requires_restart = false; + for (int i=0;iisChecked() != *bool_value.at(i)) { + bool_requires_restart = true; + break; + } + } + + // Check if any settings will require a restart of Olive (including the bool options determined above) + if (bool_requires_restart + || olive::config.thumbnail_resolution != thumbnail_res_spinbox->value() + || olive::config.waveform_resolution != waveform_res_spinbox->value() + || olive::config.css_path != custom_css_fn->text() + || olive::config.style != static_cast(ui_style->currentData().toInt())) { + + // any changes to these settings will require a restart - ask the user if we should do one now or later + + int ret = QMessageBox::question(this, + "Restart Required", + "Some of the changed settings will require a restart of Olive. Would you like " + "to restart now?", + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); + + if (ret == QMessageBox::Cancel) { + // Return to Preferences dialog without saving any settings + return; + } else if (ret == QMessageBox::Yes) { + + // Check if we can close the current project. If not, we'll treat it as if the user clicked "Cancel". + if (olive::Global->can_close_project()) { + restart_after_saving = true; + } else { + return; + } + } + // Selecting "No" will save the settings and not restart. They will become active next time Olive opens. + + } + + + // Everything checks out, start saving settings from the UI to the backend + olive::config.css_path = custom_css_fn->text(); + olive::config.recording_mode = recordingComboBox->currentIndex() + 1; + olive::config.img_seq_formats = imgSeqFormatEdit->text(); + olive::config.upcoming_queue_size = upcoming_queue_spinbox->value(); + olive::config.upcoming_queue_type = upcoming_queue_type->currentIndex(); + olive::config.previous_queue_size = previous_queue_spinbox->value(); + olive::config.previous_queue_type = previous_queue_type->currentIndex(); + + // Audio settings may require the audio device to be re-initiated. + if (olive::config.preferred_audio_output != audio_output_devices->currentData().toString() + || olive::config.preferred_audio_input != audio_input_devices->currentData().toString() + || olive::config.audio_rate != audio_sample_rate->currentData().toInt()) { + reinit_audio = true; + } + olive::config.preferred_audio_output = audio_output_devices->currentData().toString(); + olive::config.preferred_audio_input = audio_input_devices->currentData().toString(); + olive::config.audio_rate = audio_sample_rate->currentData().toInt(); + + olive::config.effect_textbox_lines = effect_textbox_lines_field->value(); + + // see if the language file should be reloaded (not necessary if the app is restarting anyway) + if (!restart_after_saving + && olive::config.language_file != language_combobox->currentData().toString()) { + reload_language = true; + } + olive::config.language_file = language_combobox->currentData().toString(); + + // Check whether OCIO settings will require a reset of the render threads + if (olive::config.playback_bit_depth != playback_bit_depth->currentIndex() + || olive::config.export_bit_depth != export_bit_depth->currentIndex()) { + reset_render_threads = true; + } + if (olive::config.ocio_config_path != ocio_config_file->text() + || olive::config.ocio_display != ocio_display->currentText() + || olive::config.ocio_view != ocio_view->currentText() + || olive::config.ocio_look != ocio_look->currentData().toString()) { + reset_ocio_shaders = true; + } + if (olive::config.ocio_config_path != ocio_config_file->text()) { + OCIO::SetCurrentConfig(OCIO::Config::CreateFromFile(ocio_config_file->text().toUtf8())); + olive::config.ocio_config_path = ocio_config_file->text(); + } + olive::config.enable_color_management = enable_color_management->isChecked(); + olive::config.playback_bit_depth = playback_bit_depth->currentIndex(); + olive::config.export_bit_depth = export_bit_depth->currentIndex(); + olive::config.ocio_display = ocio_display->currentText(); + olive::config.ocio_default_input_colorspace = ocio_default_input->currentText(); + olive::config.ocio_view = ocio_view->currentText(); + + // We use data here instead of text because there's a "(None)" option with an empty string + olive::config.ocio_look = ocio_look->currentData().toString(); + + + // Set default sequence options + olive::config.default_sequence_width = default_sequence.width(); + olive::config.default_sequence_height = default_sequence.height(); + olive::config.default_sequence_framerate = default_sequence.frame_rate(); + olive::config.default_sequence_audio_frequency = default_sequence.audio_frequency(); + olive::config.default_sequence_audio_channel_layout = default_sequence.audio_layout(); + + // Set all bool options + for (int i=0;iisChecked(); + } + + // Set new style + olive::config.style = static_cast(ui_style->currentData().toInt()); + + // Check if the thumbnail or waveform icon fields have changed, we may need to recreate the previews if so + if (olive::config.thumbnail_resolution != thumbnail_res_spinbox->value() + || olive::config.waveform_resolution != waveform_res_spinbox->value()) { + // we're changing the size of thumbnails and waveforms, so let's delete them and regenerate them next start + + // delete nothing + PreviewDeleteTypes delete_type = DELETE_NONE; + + if (olive::config.thumbnail_resolution != thumbnail_res_spinbox->value()) { + // delete existing thumbnails + olive::config.thumbnail_resolution = thumbnail_res_spinbox->value(); + + // delete only thumbnails + delete_type = DELETE_THUMBNAILS; + } + + if (olive::config.waveform_resolution != waveform_res_spinbox->value()) { + // delete existing waveforms + olive::config.waveform_resolution = waveform_res_spinbox->value(); + + // if we're already deleting thumbnails + if (delete_type == DELETE_THUMBNAILS) { + // delete all + delete_type = DELETE_BOTH; + } else { + // just delete waveforms + delete_type = DELETE_WAVEFORMS; + } + } + + delete_previews(delete_type); + } + + // Save keyboard shortcuts + for (int i=0;iset_action_shortcut(); + } + + QDialog::accept(); + + if (restart_after_saving) { + + // since we already ran can_close_project(), bypass checking again by running set_modified(false) + olive::Global->set_modified(false); + + olive::MainWindow->close(); + + QProcess::startDetached(QApplication::applicationFilePath(), { olive::ActiveProjectFilename }); + } else { + + // Audio settings may require the audio device to be re-initiated. + if (reinit_audio) { + init_audio(); + } + + if (reload_effects) { + panel_effect_controls->Reload(); + } + + // reload language file if it changed + if (reload_language) { + olive::Global->load_translation_from_config(); + } + + if (reset_render_threads) { + if (panel_footage_viewer->seq != nullptr) { + panel_footage_viewer->seq->Close(); + } + panel_footage_viewer->viewer_widget()->get_renderer()->delete_ctx(); + if (panel_sequence_viewer->seq != nullptr) { + panel_sequence_viewer->seq->Close(); + } + panel_sequence_viewer->viewer_widget()->get_renderer()->delete_ctx(); + } else if (reset_ocio_shaders) { + panel_footage_viewer->viewer_widget()->get_renderer()->destroy_ocio(); + panel_sequence_viewer->viewer_widget()->get_renderer()->destroy_ocio(); + } + + } +} + +void PreferencesDialog::reset_default_shortcut() { + QList items = keyboard_tree->selectedItems(); + for (int i=0;iselectedItems().at(i); + static_cast(keyboard_tree->itemWidget(item, 1))->reset_to_default(); + } +} + +void PreferencesDialog::reset_all_shortcuts() { + if (QMessageBox::question( + this, + tr("Confirm Reset All Shortcuts"), + tr("Are you sure you wish to reset all keyboard shortcuts to their defaults?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + for (int i=0;ireset_to_default(); + } + } +} + +bool PreferencesDialog::refine_shortcut_list(const QString &s, QTreeWidgetItem* parent) { + if (parent == nullptr) { + for (int i=0;itopLevelItemCount();i++) { + refine_shortcut_list(s, keyboard_tree->topLevelItem(i)); + } + } else { + parent->setExpanded(!s.isEmpty()); + + bool all_children_are_hidden = !s.isEmpty(); + + for (int i=0;ichildCount();i++) { + QTreeWidgetItem* item = parent->child(i); + if (item->childCount() > 0) { + all_children_are_hidden = refine_shortcut_list(s, item); + } else { + item->setHidden(false); + if (s.isEmpty()) { + all_children_are_hidden = false; + } else { + QString shortcut; + if (keyboard_tree->itemWidget(item, 1) != nullptr) { + shortcut = static_cast(keyboard_tree->itemWidget(item, 1))->keySequence().toString(); + } + if (item->text(0).contains(s, Qt::CaseInsensitive) || shortcut.contains(s, Qt::CaseInsensitive)) { + all_children_are_hidden = false; + } else { + item->setHidden(true); + } + } + } + } + + if (parent->text(0).contains(s, Qt::CaseInsensitive)) all_children_are_hidden = false; + + parent->setHidden(all_children_are_hidden); + + return all_children_are_hidden; + } + return true; +} + +void PreferencesDialog::load_shortcut_file() { + QString fn = QFileDialog::getOpenFileName(this, tr("Import Keyboard Shortcuts")); + if (!fn.isEmpty()) { + QFile f(fn); + if (f.exists() && f.open(QFile::ReadOnly)) { + QByteArray ba = f.readAll(); + f.close(); + for (int i=0;iaction_name()); + if (index == 0 || (index > 0 && ba.at(index-1) == '\n')) { + while (index < ba.size() && ba.at(index) != '\t') index++; + QString ks; + index++; + while (index < ba.size() && ba.at(index) != '\n') { + ks.append(ba.at(index)); + index++; + } + key_shortcut_fields.at(i)->setKeySequence(ks); + } else { + key_shortcut_fields.at(i)->reset_to_default(); + } + } + } else { + QMessageBox::critical( + this, + tr("Error saving shortcuts"), + tr("Failed to open file for reading") + ); + } + } +} + +void PreferencesDialog::save_shortcut_file() { + QString fn = QFileDialog::getSaveFileName(this, tr("Export Keyboard Shortcuts")); + if (!fn.isEmpty()) { + QFile f(fn); + if (f.open(QFile::WriteOnly)) { + bool start = true; + for (int i=0;iexport_shortcut(); + if (!s.isEmpty()) { + if (!start) f.write("\n"); + f.write(s.toUtf8()); + start = false; + } + } + f.close(); + QMessageBox::information(this, tr("Export Shortcuts"), tr("Shortcuts exported successfully")); + } else { + QMessageBox::critical(this, tr("Error saving shortcuts"), tr("Failed to open file for writing")); + } + } +} + +void PreferencesDialog::browse_css_file() { + QString fn = QFileDialog::getOpenFileName(this, tr("Browse for CSS file")); + if (!fn.isEmpty()) { + custom_css_fn->setText(fn); + } +} + +void PreferencesDialog::browse_ocio_config() +{ + QString fn = QFileDialog::getOpenFileName(this, tr("Browse for OpenColorIO configuration")); + if (!fn.isEmpty()) { + ocio_config_file->setText(fn); + enable_color_management->setChecked(true); + } +} + +void PreferencesDialog::update_ocio_view_menu() +{ + update_ocio_view_menu(OCIO::GetCurrentConfig()); +} + +void PreferencesDialog::delete_all_previews() { + if (QMessageBox::question(this, + tr("Delete All Previews"), + tr("Are you sure you want to delete all previews?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + delete_previews(DELETE_BOTH); + QMessageBox::information(this, + tr("Previews Deleted"), + tr("All previews deleted successfully. You may have to re-open your current project for " + "changes to take effect."), + QMessageBox::Ok); + } +} + +void PreferencesDialog::edit_default_sequence_settings() +{ + NewSequenceDialog nsd(this, nullptr, &default_sequence); + nsd.SetNameEditable(false); + nsd.exec(); +} + +void PreferencesDialog::setup_ui() { + QVBoxLayout* verticalLayout = new QVBoxLayout(this); + QTabWidget* tabWidget = new QTabWidget(this); + + // row counter used to ease adding new rows + int row = 0; + + // General + QWidget* general_tab = new QWidget(this); + QGridLayout* general_layout = new QGridLayout(general_tab); + + // General -> Language + general_layout->addWidget(new QLabel(tr("Language:")), row, 0); + + language_combobox = new QComboBox(); + + // add default language (en-US) + language_combobox->addItem(QLocale::languageToString(QLocale("en-US").language())); + + // add languages from file + QList translation_paths = get_language_paths(); + + // iterate through all language search paths + for (int j=0;jaddItem(QLocale(locale_str).nativeLanguageName(), locale_relative_path); + + if (olive::config.language_file == locale_relative_path) { + language_combobox->setCurrentIndex(language_combobox->count() - 1); + } + } + } + } + + general_layout->addWidget(language_combobox, row, 1, 1, 4); + + row++; + + // General -> Image Sequence Formats + general_layout->addWidget(new QLabel(tr("Image sequence formats:"), this), row, 0); + + imgSeqFormatEdit = new QLineEdit(general_tab); + imgSeqFormatEdit->setText(olive::config.img_seq_formats); + general_layout->addWidget(imgSeqFormatEdit, row, 1, 1, 4); + + row++; + + // General -> Thumbnail and Waveform Resolution + general_layout->addWidget(new QLabel(tr("Thumbnail Resolution:"), this), row, 0); + + thumbnail_res_spinbox = new QSpinBox(this); + thumbnail_res_spinbox->setMinimum(0); + thumbnail_res_spinbox->setMaximum(INT_MAX); + thumbnail_res_spinbox->setValue(olive::config.thumbnail_resolution); + general_layout->addWidget(thumbnail_res_spinbox, row, 1); + + general_layout->addWidget(new QLabel(tr("Waveform Resolution:"), this), row, 2); + + waveform_res_spinbox = new QSpinBox(this); + waveform_res_spinbox->setMinimum(0); + waveform_res_spinbox->setMaximum(INT_MAX); + waveform_res_spinbox->setValue(olive::config.waveform_resolution); + general_layout->addWidget(waveform_res_spinbox, row, 3); + + QPushButton* delete_preview_btn = new QPushButton(tr("Delete Previews")); + general_layout->addWidget(delete_preview_btn, row, 4); + connect(delete_preview_btn, SIGNAL(clicked(bool)), this, SLOT(delete_all_previews())); + + row++; + + QHBoxLayout* misc_general = new QHBoxLayout(); + + // General -> Use Software Fallbacks When Possible + QCheckBox* use_software_fallbacks_checkbox = new QCheckBox(tr("Use Software Fallbacks When Possible")); + AddBoolPair(use_software_fallbacks_checkbox, &olive::config.use_software_fallback, true); + misc_general->addWidget(use_software_fallbacks_checkbox); + + // General -> Don't Use Proxies When Exporting + QCheckBox* dont_use_proxies_when_exporting = new QCheckBox(tr("Don't Use Proxies When Exporting")); + dont_use_proxies_when_exporting->setToolTip(tr("Use originals instead of proxies when exporting")); + AddBoolPair(dont_use_proxies_when_exporting, &olive::config.dont_use_proxies_on_export); + misc_general->addWidget(dont_use_proxies_when_exporting); + + // General -> Default Sequence Settings + QPushButton* default_sequence_settings = new QPushButton(tr("Default Sequence Settings")); + connect(default_sequence_settings, SIGNAL(clicked(bool)), this, SLOT(edit_default_sequence_settings())); + misc_general->addWidget(default_sequence_settings); + + general_layout->addLayout(misc_general, row, 0, 1, 5); + + row++; + + tabWidget->addTab(general_tab, tr("General")); + + // Behavior + QWidget* behavior_tab = new QWidget(this); + tabWidget->addTab(behavior_tab, tr("Behavior")); + + ColumnedGridLayout* behavior_tab_layout = new ColumnedGridLayout(behavior_tab, 2); + + QCheckBox* add_default_effects_to_clips = new QCheckBox(tr("Add Default Effects to New Clips")); + AddBoolPair(add_default_effects_to_clips, &olive::config.add_default_effects_to_clips); + behavior_tab_layout->Add(add_default_effects_to_clips); + + QCheckBox* auto_seek_to_beginning = new QCheckBox(tr("Automatically Seek to the Beginning When Playing at the End of a Sequence")); + AddBoolPair(auto_seek_to_beginning, &olive::config.auto_seek_to_beginning); + behavior_tab_layout->Add(auto_seek_to_beginning); + + QCheckBox* selecting_also_seeks = new QCheckBox(tr("Selecting Also Seeks")); + AddBoolPair(selecting_also_seeks, &olive::config.select_also_seeks); + behavior_tab_layout->Add(selecting_also_seeks); + + QCheckBox* edit_tool_also_seeks = new QCheckBox(tr("Edit Tool Also Seeks")); + AddBoolPair(edit_tool_also_seeks, &olive::config.edit_tool_also_seeks); + behavior_tab_layout->Add(edit_tool_also_seeks); + + QCheckBox* edit_tool_selects_links = new QCheckBox(tr("Edit Tool Selects Links")); + AddBoolPair(edit_tool_selects_links, &olive::config.edit_tool_selects_links); + behavior_tab_layout->Add(edit_tool_selects_links); + + QCheckBox* seek_also_selects = new QCheckBox(tr("Seek Also Selects")); + AddBoolPair(seek_also_selects, &olive::config.seek_also_selects); + behavior_tab_layout->Add(seek_also_selects); + + QCheckBox* seek_to_end_of_pastes = new QCheckBox(tr("Seek to the End of Pastes")); + AddBoolPair(seek_to_end_of_pastes, &olive::config.paste_seeks); + behavior_tab_layout->Add(seek_to_end_of_pastes); + + QCheckBox* scroll_wheel_zooms = new QCheckBox(tr("Scroll Wheel Zooms")); + scroll_wheel_zooms->setToolTip(tr("Hold CTRL to toggle this setting")); + AddBoolPair(scroll_wheel_zooms, &olive::config.scroll_zooms); + behavior_tab_layout->Add(scroll_wheel_zooms); + + QCheckBox* invert_timeline_scroll_axes = new QCheckBox(tr("Invert Timeline Scroll Axes")); + AddBoolPair(invert_timeline_scroll_axes, &olive::config.invert_timeline_scroll_axes); + behavior_tab_layout->Add(invert_timeline_scroll_axes); + + QCheckBox* enable_drag_files_to_timeline = new QCheckBox(tr("Enable Drag Files to Timeline")); + AddBoolPair(enable_drag_files_to_timeline, &olive::config.enable_drag_files_to_timeline); + behavior_tab_layout->Add(enable_drag_files_to_timeline); + + QCheckBox* autoscale_by_default = new QCheckBox(tr("Auto-Scale By Default")); + AddBoolPair(autoscale_by_default, &olive::config.autoscale_by_default); + behavior_tab_layout->Add(autoscale_by_default); + + QCheckBox* enable_seek_to_import = new QCheckBox(tr("Auto-Seek to Imported Clips")); + AddBoolPair(enable_seek_to_import, &olive::config.enable_seek_to_import); + behavior_tab_layout->Add(enable_seek_to_import); + + QCheckBox* enable_audio_scrubbing = new QCheckBox(tr("Audio Scrubbing")); + AddBoolPair(enable_audio_scrubbing, &olive::config.enable_audio_scrubbing); + behavior_tab_layout->Add(enable_audio_scrubbing); + + QCheckBox* enable_drop_on_media_to_replace = new QCheckBox(tr("Drop Files on Media to Replace")); + AddBoolPair(enable_drop_on_media_to_replace, &olive::config.drop_on_media_to_replace); + behavior_tab_layout->Add(enable_drop_on_media_to_replace); + + QCheckBox* enable_hover_focus = new QCheckBox(tr("Enable Hover Focus")); + AddBoolPair(enable_hover_focus, &olive::config.hover_focus); + behavior_tab_layout->Add(enable_hover_focus); + + QCheckBox* set_name_and_marker = new QCheckBox(tr("Ask For Name When Setting Marker")); + AddBoolPair(set_name_and_marker, &olive::config.set_name_with_marker); + behavior_tab_layout->Add(set_name_and_marker); + + // Appearance + QWidget* appearance_tab = new QWidget(this); + tabWidget->addTab(appearance_tab, tr("Appearance")); + + row = 0; + + QGridLayout* appearance_layout = new QGridLayout(appearance_tab); + + // Appearance -> Theme + appearance_layout->addWidget(new QLabel(tr("Theme")), row, 0); + + ui_style = new QComboBox(); + ui_style->addItem(tr("Olive Dark (Default)"), olive::styling::kOliveDefaultDark); + ui_style->addItem(tr("Olive Light"), olive::styling::kOliveDefaultLight); + ui_style->addItem(tr("Native"), olive::styling::kNativeDarkIcons); + ui_style->addItem(tr("Native (Light Icons)"), olive::styling::kNativeLightIcons); + ui_style->setCurrentIndex(olive::config.style); + appearance_layout->addWidget(ui_style, row, 1, 1, 2); + + row++; + +#ifdef Q_OS_WIN + // Native menu styling is only available on Windows. Environments like Ubuntu and Mac use the native menu system by + // default + QCheckBox* native_menus = new QCheckBox(tr("Use Native Menu Styling")); + AddBoolPair(native_menus, &olive::config.use_native_menu_styling, true); + appearance_layout->addWidget(native_menus, row, 0, 1, 3); + + row++; +#endif + + // Appearance -> Custom CSS + appearance_layout->addWidget(new QLabel(tr("Custom CSS:"), this), row, 0); + + custom_css_fn = new QLineEdit(general_tab); + custom_css_fn->setText(olive::config.css_path); + appearance_layout->addWidget(custom_css_fn, row, 1); + + QPushButton* custom_css_browse = new QPushButton(tr("Browse"), general_tab); + connect(custom_css_browse, SIGNAL(clicked(bool)), this, SLOT(browse_css_file())); + appearance_layout->addWidget(custom_css_browse, row, 2); + + row++; + + // Appearance -> Effect Textbox Lines + appearance_layout->addWidget(new QLabel(tr("Effect Textbox Lines:"), this), row, 0); + + effect_textbox_lines_field = new QSpinBox(general_tab); + effect_textbox_lines_field->setMinimum(1); + effect_textbox_lines_field->setValue(olive::config.effect_textbox_lines); + appearance_layout->addWidget(effect_textbox_lines_field, row, 1, 1, 2); + + row++; + + // Playback + QWidget* playback_tab = new QWidget(this); + QVBoxLayout* playback_tab_layout = new QVBoxLayout(playback_tab); + + // Playback -> Memory Usage + QGroupBox* memory_usage_group = new QGroupBox(playback_tab); + memory_usage_group->setTitle(tr("Memory Usage")); + QGridLayout* memory_usage_layout = new QGridLayout(memory_usage_group); + memory_usage_layout->addWidget(new QLabel(tr("Upcoming Frame Queue:"), playback_tab), 0, 0); + upcoming_queue_spinbox = new QDoubleSpinBox(playback_tab); + upcoming_queue_spinbox->setValue(olive::config.upcoming_queue_size); + memory_usage_layout->addWidget(upcoming_queue_spinbox, 0, 1); + upcoming_queue_type = new QComboBox(playback_tab); + upcoming_queue_type->addItem(tr("frames")); + upcoming_queue_type->addItem(tr("seconds")); + upcoming_queue_type->setCurrentIndex(olive::config.upcoming_queue_type); + memory_usage_layout->addWidget(upcoming_queue_type, 0, 2); + memory_usage_layout->addWidget(new QLabel(tr("Previous Frame Queue:"), playback_tab), 1, 0); + previous_queue_spinbox = new QDoubleSpinBox(playback_tab); + previous_queue_spinbox->setValue(olive::config.previous_queue_size); + memory_usage_layout->addWidget(previous_queue_spinbox, 1, 1); + previous_queue_type = new QComboBox(playback_tab); + previous_queue_type->addItem(tr("frames")); + previous_queue_type->addItem(tr("seconds")); + previous_queue_type->setCurrentIndex(olive::config.previous_queue_type); + memory_usage_layout->addWidget(previous_queue_type, 1, 2); + playback_tab_layout->addWidget(memory_usage_group); + + tabWidget->addTab(playback_tab, tr("Playback")); + + // Audio + QWidget* audio_tab = new QWidget(this); + + QGridLayout* audio_tab_layout = new QGridLayout(audio_tab); + + row = 0; + + // Audio -> Output Device + + audio_tab_layout->addWidget(new QLabel(tr("Output Device:")), row, 0); + + audio_output_devices = new QComboBox(); + audio_output_devices->addItem(tr("Default"), ""); + + // list all available audio output devices + QList devs = QAudioDeviceInfo::availableDevices(QAudio::AudioOutput); + bool found_preferred_device = false; + for (int i=0;iaddItem(devs.at(i).deviceName(), devs.at(i).deviceName()); + if (!found_preferred_device + && devs.at(i).deviceName() == olive::config.preferred_audio_output) { + audio_output_devices->setCurrentIndex(audio_output_devices->count()-1); + found_preferred_device = true; + } + } + + audio_tab_layout->addWidget(audio_output_devices, row, 1); + + row++; + + // Audio -> Input Device + + audio_tab_layout->addWidget(new QLabel(tr("Input Device:")), row, 0); + + audio_input_devices = new QComboBox(); + audio_input_devices->addItem(tr("Default"), ""); + + // list all available audio input devices + devs = QAudioDeviceInfo::availableDevices(QAudio::AudioInput); + found_preferred_device = false; + for (int i=0;iaddItem(devs.at(i).deviceName(), devs.at(i).deviceName()); + if (!found_preferred_device + && devs.at(i).deviceName() == olive::config.preferred_audio_input) { + audio_input_devices->setCurrentIndex(audio_input_devices->count()-1); + found_preferred_device = true; + } + } + + audio_tab_layout->addWidget(audio_input_devices, row, 1); + + row++; + + // Audio -> Sample Rate + + audio_tab_layout->addWidget(new QLabel(tr("Sample Rate:")), row, 0); + + audio_sample_rate = new QComboBox(); + combobox_audio_sample_rates(audio_sample_rate); + for (int i=0;icount();i++) { + if (audio_sample_rate->itemData(i).toInt() == olive::config.audio_rate) { + audio_sample_rate->setCurrentIndex(i); + break; + } + } + + audio_tab_layout->addWidget(audio_sample_rate, row, 1); + + row++; + + // Audio -> Audio Recording + audio_tab_layout->addWidget(new QLabel(tr("Audio Recording:"), this), row, 0); + + recordingComboBox = new QComboBox(general_tab); + recordingComboBox->addItem(tr("Mono")); + recordingComboBox->addItem(tr("Stereo")); + recordingComboBox->setCurrentIndex(olive::config.recording_mode - 1); + audio_tab_layout->addWidget(recordingComboBox, row, 1); + + row++; + + tabWidget->addTab(audio_tab, tr("Audio")); + + // + // COLOR MANAGEMENT + // + + QWidget* color_management_tab = new QWidget(); + + QGridLayout* color_management_layout = new QGridLayout(color_management_tab); + + row = 0; + + // COLOR MANAGEMENT -> Enable Color Management + enable_color_management = new QCheckBox(tr("Enable Color Management")); + enable_color_management->setChecked(olive::config.enable_color_management); + color_management_layout->addWidget(enable_color_management, row, 0); + + row++; + + QGroupBox* opencolorio_groupbox = new QGroupBox(); + QGridLayout* opencolorio_groupbox_layout = new QGridLayout(opencolorio_groupbox); + + // COLOR MANAGEMENT -> OpenColorIO Config File + opencolorio_groupbox_layout->addWidget(new QLabel(tr("OpenColorIO Config File:")), 0, 0); + + ocio_config_file = new QLineEdit(); + ocio_config_file->setText(olive::config.ocio_config_path); + connect(ocio_config_file, SIGNAL(textChanged(const QString &)), this, SLOT(update_ocio_config(const QString&))); + opencolorio_groupbox_layout->addWidget(ocio_config_file, 0, 1, 1, 4); + + QPushButton* ocio_config_browse_btn = new QPushButton(tr("Browse")); + connect(ocio_config_browse_btn, SIGNAL(clicked(bool)), this, SLOT(browse_ocio_config())); + opencolorio_groupbox_layout->addWidget(ocio_config_browse_btn, 0, 5); + + // COLOR MANAGEMENT -> Default Input Color Space + ocio_default_input = new QComboBox(); + opencolorio_groupbox_layout->addWidget(new QLabel(tr("Default Input Color Space:")), 1, 0); + opencolorio_groupbox_layout->addWidget(ocio_default_input, 1, 1, 1, 5); + + // COLOR MANAGEMENT -> Display + ocio_display = new QComboBox(); + connect(ocio_display, SIGNAL(currentIndexChanged(int)), this, SLOT(update_ocio_view_menu())); + opencolorio_groupbox_layout->addWidget(new QLabel(tr("Display:")), 2, 0); + opencolorio_groupbox_layout->addWidget(ocio_display, 2, 1); + + // COLOR MANAGEMENT -> View + ocio_view = new QComboBox(); + opencolorio_groupbox_layout->addWidget(new QLabel(tr("View:")), 2, 2); + opencolorio_groupbox_layout->addWidget(ocio_view, 2, 3); + + // COLOR MANAGEMENT -> Look + ocio_look = new QComboBox(); + opencolorio_groupbox_layout->addWidget(new QLabel(tr("Look:")), 2, 4); + opencolorio_groupbox_layout->addWidget(ocio_look, 2, 5); + + color_management_layout->addWidget(opencolorio_groupbox, row, 0); + + row++; + + // COLOR MANAGEMENT -> Bit Depth + QGroupBox* bit_depth_groupbox = new QGroupBox(tr("Bit Depth")); + QGridLayout* bit_depth_groupbox_layout = new QGridLayout(bit_depth_groupbox); + + // COLOR MANAGEMENT -> Bit Depth -> Playback + playback_bit_depth = new QComboBox(); + for (int i=0;iaddItem(olive::pixel_formats.at(i).name, i); + } + playback_bit_depth->setCurrentIndex(olive::config.playback_bit_depth); + bit_depth_groupbox_layout->addWidget(new QLabel(tr("Playback (Offline):")), 0, 0); + bit_depth_groupbox_layout->addWidget(playback_bit_depth, 0, 1); + + // COLOR MANAGEMENT -> Bit Depth -> Export + export_bit_depth = new QComboBox(); + for (int i=0;iaddItem(olive::pixel_formats.at(i).name, i); + } + export_bit_depth->setCurrentIndex(olive::config.export_bit_depth); + bit_depth_groupbox_layout->addWidget(new QLabel(tr("Export (Online):")), 0, 2); + bit_depth_groupbox_layout->addWidget(export_bit_depth, 0, 3); + + color_management_layout->addWidget(bit_depth_groupbox, row, 0); + + + + //row++; + + populate_ocio_menus(OCIO::GetCurrentConfig()); + + tabWidget->addTab(color_management_tab, tr("Color Management")); + + // Shortcuts + QWidget* shortcut_tab = new QWidget(this); + + QVBoxLayout* shortcut_layout = new QVBoxLayout(shortcut_tab); + + QLineEdit* key_search_line = new QLineEdit(shortcut_tab); + key_search_line->setPlaceholderText(tr("Search for action or shortcut")); + connect(key_search_line, SIGNAL(textChanged(const QString &)), this, SLOT(refine_shortcut_list(const QString &))); + + shortcut_layout->addWidget(key_search_line); + + keyboard_tree = new QTreeWidget(shortcut_tab); + QTreeWidgetItem* tree_header = keyboard_tree->headerItem(); + tree_header->setText(0, tr("Action")); + tree_header->setText(1, tr("Shortcut")); + shortcut_layout->addWidget(keyboard_tree); + + QHBoxLayout* reset_shortcut_layout = new QHBoxLayout(shortcut_tab); + + QPushButton* import_shortcut_button = new QPushButton(tr("Import"), shortcut_tab); + reset_shortcut_layout->addWidget(import_shortcut_button); + connect(import_shortcut_button, SIGNAL(clicked(bool)), this, SLOT(load_shortcut_file())); + + QPushButton* export_shortcut_button = new QPushButton(tr("Export"), shortcut_tab); + reset_shortcut_layout->addWidget(export_shortcut_button); + connect(export_shortcut_button, SIGNAL(clicked(bool)), this, SLOT(save_shortcut_file())); + + reset_shortcut_layout->addStretch(); + + QPushButton* reset_selected_shortcut_button = new QPushButton(tr("Reset Selected"), shortcut_tab); + reset_shortcut_layout->addWidget(reset_selected_shortcut_button); + connect(reset_selected_shortcut_button, SIGNAL(clicked(bool)), this, SLOT(reset_default_shortcut())); + + QPushButton* reset_all_shortcut_button = new QPushButton(tr("Reset All"), shortcut_tab); + reset_shortcut_layout->addWidget(reset_all_shortcut_button); + connect(reset_all_shortcut_button, SIGNAL(clicked(bool)), this, SLOT(reset_all_shortcuts())); + + shortcut_layout->addLayout(reset_shortcut_layout); + + tabWidget->addTab(shortcut_tab, tr("Keyboard")); + + verticalLayout->addWidget(tabWidget); + + QDialogButtonBox* buttonBox = new QDialogButtonBox(this); + buttonBox->setOrientation(Qt::Horizontal); + buttonBox->setStandardButtons(QDialogButtonBox::Cancel|QDialogButtonBox::Ok); + + verticalLayout->addWidget(buttonBox); + + connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept())); + connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); +} diff --git a/app/dialog/preferences/preferences.h b/app/dialog/preferences/preferences.h new file mode 100644 index 000000000..721a2b505 --- /dev/null +++ b/app/dialog/preferences/preferences.h @@ -0,0 +1,432 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef PREFERENCESDIALOG_H +#define PREFERENCESDIALOG_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace OCIO = OCIO_NAMESPACE::v1; + +#include "timeline/sequence.h" + +class KeySequenceEditor; + +/** + * @brief The PreferencesDialog class + * + * A dialog for the global application settings. Mostly an interface for Config. Can be loaded from any part of the + * application. + */ +class PreferencesDialog : public QDialog +{ + Q_OBJECT + +public: + /** + * @brief PreferencesDialog Constructor + * + * @param parent + * + * QWidget parent. Usually MainWindow. + */ + explicit PreferencesDialog(QWidget *parent = nullptr); + +private slots: + /** + * @brief Override of accept to save preferences to Config. + */ + virtual void accept() override; + + /** + * @brief Reset all selected shortcuts in keyboard_tree to their defaults + */ + void reset_default_shortcut(); + + /** + * @brief Reset all shortcuts indiscriminately to their defaults + * + * This is safe to call directly as it'll ask the user if they wish to do so before it resets. + */ + void reset_all_shortcuts(); + + /** + * @brief Shows/hides shortcut entries according to a shortcut query. + * + * This function can be directly connected to QLineEdit::textChanged() for simplicity. + * + * @param s + * + * The search query to compare shortcut names to. + * + * @param parent + * + * This is used as the function calls itself recursively to traverse the menu item hierarchy. This should be left as + * nullptr when called externally. + * + * @return + * + * Value used as function calls itself recursively to determine if a menu parent has any children that are not hidden. + * If so, TRUE is returned so the parent is shown too (even if it doesn't match the search query). If not, FALSE is + * returned so the parent is hidden. + */ + bool refine_shortcut_list(const QString &s, QTreeWidgetItem* parent = nullptr); + + /** + * @brief Show a file dialog to load an external shortcut preset from file + */ + void load_shortcut_file(); + + /** + * @brief Show a file dialog to save an external shortcut preset from file + */ + void save_shortcut_file(); + + /** + * @brief Delete all previews (waveform and thumbnail cache) + */ + void delete_all_previews(); + + // Browse for file functionns + /** + * @brief Show a file dialog to browse for an external CSS file to load for styling the application. + */ + void browse_css_file(); + void browse_ocio_config(); + + // OCIO function + void update_ocio_view_menu(); + void update_ocio_view_menu(OCIO::ConstConfigRcPtr config); + void update_ocio_config(const QString&); + + /** + * @brief Shows a NewSequenceDialog attached to default_sequence + */ + void edit_default_sequence_settings(); + +private: + + /** + * @brief Create and arrange all UI widgets + */ + void setup_ui(); + + /** + * @brief Populate keyboard shortcut panel with keyboard shortcuts from the menu bar + * + * @param menu + * + * A reference to the main application's menu bar. Usually MainWindow::menuBar(). + */ + void setup_kbd_shortcuts(QMenuBar* menu); + + /** + * @brief Internal function called by setup_kbd_shortcuts() to traverse down the menu bar's hierarchy and populate the + * shortcut panel. + * + * This function will call itself recursively as it finds submenus belong to the menu provided. It will also create + * QTreeWidgetItems as children of the parent item provided, either using them as parents themselves for submenus + * or attaching a KeySequenceEditor to them for shortcut editing. + * + * @param menu + * + * The current menu to traverse down. + * + * @param parent + * + * The parent item to add QTreeWidgetItems to. + */ + void setup_kbd_shortcut_worker(QMenu* menu, QTreeWidgetItem* parent); + + enum PreviewDeleteTypes { + DELETE_NONE, + DELETE_THUMBNAILS, + DELETE_WAVEFORMS, + DELETE_BOTH + }; + + // used to delete previews + // type can be: 't' for thumbnails, 'w' for waveforms, or 1 for all + /** + * @brief Delete disk cached preview files (thumbnails, waveforms, etc.) + * + * @param type + * + * The types of previews to delete. + */ + void delete_previews(PreviewDeleteTypes type); + + void populate_ocio_menus(OCIO::ConstConfigRcPtr config); + + /** + * @brief UI widget for editing the CSS filename + */ + QLineEdit* custom_css_fn; + + /** + * @brief UI widget for editing the list of extensions to detect image sequences from + */ + QLineEdit* imgSeqFormatEdit; + + /** + * @brief UI widget for editing the recording channels + */ + QComboBox* recordingComboBox; + + + /** + * @brief UI widget for editing keyboard shortcuts + */ + QTreeWidget* keyboard_tree; + + /** + * @brief UI widget for editing the upcoming queue size + */ + QDoubleSpinBox* upcoming_queue_spinbox; + + /** + * @brief UI widget for editing the upcoming queue type + */ + QComboBox* upcoming_queue_type; + + /** + * @brief UI widget for editing the previous queue size + */ + QDoubleSpinBox* previous_queue_spinbox; + + /** + * @brief UI widget for editing the previous queue type + */ + QComboBox* previous_queue_type; + + /** + * @brief UI widget for editing the size of textboxes in the EffectControls panel + */ + QSpinBox* effect_textbox_lines_field; + + /** + * @brief UI widget for selecting the output audio device + */ + QComboBox* audio_output_devices; + + /** + * @brief UI widget for selecting the input audio device + */ + QComboBox* audio_input_devices; + + /** + * @brief UI widget for selecting the audio sampling rates + */ + QComboBox* audio_sample_rate; + + /** + * @brief UI widget for selecting the UI language + */ + QComboBox* language_combobox; + + /** + * @brief UI widget for selecting the resolution of the thumbnails to generate + */ + QSpinBox* thumbnail_res_spinbox; + + /** + * @brief UI widget for selecting the resolution of the waveforms to generate + */ + QSpinBox* waveform_res_spinbox; + + QCheckBox* enable_color_management; + QLineEdit* ocio_config_file; + QComboBox* ocio_default_input; + QComboBox* ocio_display; + QComboBox* ocio_view; + QComboBox* ocio_look; + QComboBox* playback_bit_depth; + QComboBox* export_bit_depth; + + /** + * @brief UI widget for selecting the current UI style + */ + QComboBox* ui_style; + + /** + * @brief Stored default Sequence object + * + * Default Sequence settings are loaded into an actual Sequence object that can be loaded into NewSequenceDialog + * for the sake of familiarity with the user. + */ + Sequence default_sequence; + + /** + * @brief List of keyboard shortcut actions that can be triggered (links with key_shortcut_items and + * key_shortcut_fields) + */ + QVector key_shortcut_actions; + + /** + * @brief List of keyboard shortcut items in keyboard_tree corresponding to existing actions (links with + * key_shortcut_actions and key_shortcut_fields) + */ + QVector key_shortcut_items; + + /** + * @brief List of keyboard shortcut editing fields in keyboard_tree corresponding to existing actions (links with + * key_shortcut_actions and key_shortcut_fields) + */ + QVector key_shortcut_fields; + + /** + * @brief Tests an OpenColorIO configuration file to determine whether it's valid and throws a messagebox if not + * + * @param url + * + * URL to the OpenColorIO configuration file. + * + * @return + * + * A OCIO::ConstConfigRcPtr config pointer if the configuration file is valid, nullptr if not. + */ + OCIO::ConstConfigRcPtr TestOCIOConfig(const QString& url); + + /** + * @brief Add an automated QCheckBox+boolean value pair + * + * Many preferences are simple true/false (or on/off) options. Rather than adding a QCheckBox for each one and + * manually setting its checked value to the configuration setting (and vice versa when saving), this convenience + * function will add it to an automated set of checkboxes, automatically setting the checked state to the current + * setting, and then saving the new checked state back to the setting when the user accepts the changes (clicks OK). + * + * @param ui + * + * A valid QCheckBox item. This function does not take ownership of the QWidget or place it in a layout anywhere. + * + * @param value + * + * A pointer to the Boolean value this QCheckBox should be shared with. The QCheckBox widget's checked state will be + * set to the value of this pointer. + * + * @param restart_required + * + * Defaults to FALSE, set this to TRUE if changing this setting should prompt the user for a restart of Olive before + * the setting change takes effect. + */ + void AddBoolPair(QCheckBox* ui, bool* value, bool restart_required = false); + + /** + * @brief Internal array managed by AddBoolPair(). Do not access this directly. + */ + QVector bool_ui; + + /** + * @brief Internal array managed by AddBoolPair(). Do not access this directly. + */ + QVector bool_value; + + /** + * @brief Internal array managed by AddBoolPair(). Do not access this directly. + */ + QVector bool_restart_required; +}; + +/** + * @brief The KeySequenceEditor class + * + * Simple derived class of QKeySequenceEdit that attaches to a QAction and provides functions for transferring + * keyboard shortcuts to and from it. + */ +class KeySequenceEditor : public QKeySequenceEdit { + Q_OBJECT +public: + /** + * @brief KeySequenceEditor Constructor + * + * @param parent + * + * QWidget parent. + * + * @param a + * + * The QAction to link to. This cannot be changed throughout the lifetime of a KeySequenceEditor. + */ + KeySequenceEditor(QWidget *parent, QAction* a); + + /** + * @brief Sets the attached QAction's shortcut to the shortcut entered in this field. + * + * This is not done automatically in case the user cancels out of the Preferences dialog, in which case the + * expectation is that the changes made will not be saved. Therefore, this needs to be triggered manually when + * PreferencesDialog saves. + */ + void set_action_shortcut(); + + /** + * @brief Set this shortcut back to the QAction's default shortcut + * + * Each QAction contains the default shortcut in its `property("default")` and can be used to restore the default + * "hard-coded" shortcut with this function. + * + * This function does not save the default shortcut back into the QAction, it simply loads the default shortcut from + * the QAction into this edit field. To save it into the QAction, it's necessary to call set_action_shortcut() after + * calling this function. + */ + void reset_to_default(); + + /** + * @brief Return attached QAction's unique ID + * + * Each of Olive's menu actions has a unique string ID (that, unlike the text, is not translated) for matching with + * an external shortcut configuration file. The ID is stored in the QAction's `property("id")`. This function returns + * that ID. + * + * @return + * + * The QAction's unique ID. + */ + QString action_name(); + + /** + * @brief Serialize this shortcut entry into a string that can be saved to a file + * + * @return + * + * A string serialization of this shortcut. The format is "[ID]\t[SEQUENCE]" where [ID] is the attached QAction's + * unique identifier and [SEQUENCE] is the current keyboard shortcut in the field (NOT necessarily the shortcut in + * the QAction). If the entered shortcut is the same as the QAction's default shortcut, the return value is empty + * because a default shortcut does not need to be saved to a file. + */ + QString export_shortcut(); +private: + /** + * @brief Internal reference to the linked QAction + */ + QAction* action; +}; + +#endif // PREFERENCESDIALOG_H diff --git a/app/panel/panelmanager.cpp b/app/panel/panelmanager.cpp index 899f86e87..5c0107936 100644 --- a/app/panel/panelmanager.cpp +++ b/app/panel/panelmanager.cpp @@ -36,6 +36,11 @@ void PanelManager::DeleteAllPanels() focus_history_.clear(); } +const QList &PanelManager::panels() +{ + return focus_history_; +} + PanelWidget *PanelManager::CurrentlyFocused() const { if (focus_history_.isEmpty()) { diff --git a/app/panel/panelmanager.h b/app/panel/panelmanager.h index 9093ed6de..9717af9b7 100644 --- a/app/panel/panelmanager.h +++ b/app/panel/panelmanager.h @@ -47,8 +47,20 @@ class PanelManager : public QObject public: PanelManager(QObject* parent); + /** + * @brief Destroy all panels + * + * Should only be used on application exit to cleanly free all panels. + */ void DeleteAllPanels(); + /** + * @brief Get a list of all existing panels + * + * Panels are ordered from most recently focused to least recently focused. + */ + const QList& panels(); + /** * @brief Return the currently focused widget, or nullptr if nothing is focused */ diff --git a/app/panel/timeline/timeline.cpp b/app/panel/timeline/timeline.cpp index a05beb2f0..1c4b2f687 100644 --- a/app/panel/timeline/timeline.cpp +++ b/app/panel/timeline/timeline.cpp @@ -119,6 +119,16 @@ void TimelinePanel::EditToOut() view_->EditToOut(); } +void TimelinePanel::GoToPrevCut() +{ + view_->GoToPrevCut(); +} + +void TimelinePanel::GoToNextCut() +{ + view_->GoToNextCut(); +} + void TimelinePanel::changeEvent(QEvent *e) { if (e->type() == QEvent::LanguageChange) { diff --git a/app/panel/timeline/timeline.h b/app/panel/timeline/timeline.h index 587c218af..8c9aad050 100644 --- a/app/panel/timeline/timeline.h +++ b/app/panel/timeline/timeline.h @@ -55,6 +55,10 @@ public: virtual void EditToOut() override; + virtual void GoToPrevCut() override; + + virtual void GoToNextCut() override; + public slots: void SetTimebase(const rational& timebase); diff --git a/app/panel/viewer/viewer.cpp b/app/panel/viewer/viewer.cpp index a81ddd682..fab4c6741 100644 --- a/app/panel/viewer/viewer.cpp +++ b/app/panel/viewer/viewer.cpp @@ -69,6 +69,21 @@ void ViewerPanel::GoToEnd() viewer_->GoToEnd(); } +void ViewerPanel::ShuttleLeft() +{ + viewer_->ShuttleLeft(); +} + +void ViewerPanel::ShuttleStop() +{ + viewer_->ShuttleStop(); +} + +void ViewerPanel::ShuttleRight() +{ + viewer_->ShuttleRight(); +} + void ViewerPanel::SetTimebase(const rational &timebase) { viewer_->SetTimebase(timebase); diff --git a/app/panel/viewer/viewer.h b/app/panel/viewer/viewer.h index 12abcf438..a6780a505 100644 --- a/app/panel/viewer/viewer.h +++ b/app/panel/viewer/viewer.h @@ -48,6 +48,12 @@ public: virtual void GoToEnd() override; + virtual void ShuttleLeft() override; + + virtual void ShuttleStop() override; + + virtual void ShuttleRight() override; + void SetTimebase(const rational& timebase); void ConnectViewerNode(ViewerOutput* node); diff --git a/app/undo/undostack.cpp b/app/undo/undostack.cpp index 5e290e17e..77879f00c 100644 --- a/app/undo/undostack.cpp +++ b/app/undo/undostack.cpp @@ -1,3 +1,23 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + #include "undostack.h" OliveUndoStack olive::undo_stack; diff --git a/app/undo/undostack.h b/app/undo/undostack.h index 754e9fb12..65204e6a0 100644 --- a/app/undo/undostack.h +++ b/app/undo/undostack.h @@ -1,3 +1,23 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + #ifndef UNDOSTACK_H #define UNDOSTACK_H diff --git a/app/widget/panel/panel.h b/app/widget/panel/panel.h index a90e8e0b3..febd14583 100644 --- a/app/widget/panel/panel.h +++ b/app/widget/panel/panel.h @@ -96,6 +96,16 @@ public: virtual void EditToOut(){} + virtual void ShuttleLeft(){} + + virtual void ShuttleStop(){} + + virtual void ShuttleRight(){} + + virtual void GoToPrevCut(){} + + virtual void GoToNextCut(){} + protected: /** * @brief Set panel's title diff --git a/app/widget/timelineview/timelineview.cpp b/app/widget/timelineview/timelineview.cpp index 9331597f6..bb1c1ec60 100644 --- a/app/widget/timelineview/timelineview.cpp +++ b/app/widget/timelineview/timelineview.cpp @@ -238,6 +238,69 @@ void TimelineView::EditToOut() RippleEditTo(olive::timeline::kTrimOut, true); } +void TimelineView::GoToPrevCut() +{ + if (timeline_node_ == nullptr) { + return; + } + + if (playhead_ == 0) { + return; + } + + int64_t closest_cut = 0; + + foreach (TrackOutput* track, timeline_node_->Tracks()) { + int64_t this_track_closest_cut = 0; + + foreach (Block* block, track->Blocks()) { + int64_t block_out_ts = olive::time_to_timestamp(block->out(), timebase_); + + if (block_out_ts < playhead_) { + this_track_closest_cut = block_out_ts; + } else { + break; + } + } + + closest_cut = qMax(closest_cut, this_track_closest_cut); + } + + UserSetTime(closest_cut); +} + +void TimelineView::GoToNextCut() +{ + if (timeline_node_ == nullptr) { + return; + } + + int64_t closest_cut = INT64_MAX; + + foreach (TrackOutput* track, timeline_node_->Tracks()) { + int64_t this_track_closest_cut = olive::time_to_timestamp(track->in(), timebase_); + + if (this_track_closest_cut <= playhead_) { + this_track_closest_cut = INT64_MAX; + } + + foreach (Block* block, track->Blocks()) { + int64_t block_in_ts = olive::time_to_timestamp(block->in(), timebase_); + + if (block_in_ts > playhead_) { + this_track_closest_cut = block_in_ts; + break; + } + } + + closest_cut = qMin(closest_cut, this_track_closest_cut); + } + + if (closest_cut < INT64_MAX) { + UserSetTime(closest_cut); + } +} + void TimelineView::SetTime(const int64_t time) { playhead_ = time; @@ -456,11 +519,16 @@ void TimelineView::RippleEditTo(olive::timeline::MovementMode mode, bool insert_ if (mode == olive::timeline::kTrimIn && !insert_gaps) { int64_t new_time = olive::time_to_timestamp(closest_point_to_playhead, timebase_); - SetTime(new_time); - emit TimeChanged(new_time); + UserSetTime(new_time); } } +void TimelineView::UserSetTime(const int64_t &time) +{ + SetTime(time); + emit TimeChanged(time); +} + void TimelineView::BlockChanged() { TimelineViewRect* rect = block_items_[static_cast(sender())]; diff --git a/app/widget/timelineview/timelineview.h b/app/widget/timelineview/timelineview.h index 975a91320..2b6f07fad 100644 --- a/app/widget/timelineview/timelineview.h +++ b/app/widget/timelineview/timelineview.h @@ -64,6 +64,10 @@ public: void EditToOut(); + void GoToPrevCut(); + + void GoToNextCut(); + public slots: void SetTimebase(const rational& timebase); @@ -358,6 +362,8 @@ private: void RippleEditTo(olive::timeline::MovementMode mode, bool insert_gaps); + void UserSetTime(const int64_t& time); + QGraphicsScene scene_; double scale_; diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index 8c05ad8c2..22acf0c10 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -31,7 +31,8 @@ ViewerWidget::ViewerWidget(QWidget *parent) : QWidget(parent), - viewer_node_(nullptr) + viewer_node_(nullptr), + playback_speed_(0) { // Set up main layout QVBoxLayout* layout = new QVBoxLayout(this); @@ -181,6 +182,24 @@ void ViewerWidget::UpdateTextureFromNode(const rational& time) } } +void ViewerWidget::PlayInternal(int speed) +{ + Q_ASSERT(speed != 0); + + if (time_base_.isNull()) { + qWarning() << "ViewerWidget can't play with an invalid timebase"; + return; + } + + start_msec_ = QDateTime::currentMSecsSinceEpoch(); + start_timestamp_ = ruler_->GetTime(); + playback_speed_ = speed; + + playback_timer_.start(); + + controls_->ShowPauseButton(); +} + void ViewerWidget::RulerTimeChange(int64_t i) { Pause(); @@ -190,17 +209,7 @@ void ViewerWidget::RulerTimeChange(int64_t i) void ViewerWidget::Play() { - if (time_base_.isNull()) { - qWarning() << "ViewerWidget can't play with an invalid timebase"; - return; - } - - start_msec_ = QDateTime::currentMSecsSinceEpoch(); - start_timestamp_ = ruler_->GetTime(); - - playback_timer_.start(); - - controls_->ShowPauseButton(); + PlayInternal(1); } void ViewerWidget::Pause() @@ -208,6 +217,8 @@ void ViewerWidget::Pause() playback_timer_.stop(); controls_->ShowPlayButton(); + + playback_speed_ = 0; } void ViewerWidget::GoToStart() @@ -238,13 +249,55 @@ void ViewerWidget::GoToEnd() qWarning() << "No end frame support yet"; } +void ViewerWidget::ShuttleLeft() +{ + int current_speed = playback_speed_; + + if (current_speed != 0) { + Pause(); + } + + current_speed--; + + if (current_speed != 0) { + PlayInternal(current_speed); + } +} + +void ViewerWidget::ShuttleStop() +{ + Pause(); +} + +void ViewerWidget::ShuttleRight() +{ + int current_speed = playback_speed_; + + if (current_speed != 0) { + Pause(); + } + + current_speed++; + + if (current_speed != 0) { + PlayInternal(current_speed); + } +} + void ViewerWidget::PlaybackTimerUpdate() { int64_t real_time = QDateTime::currentMSecsSinceEpoch() - start_msec_; int64_t frames_since_start = qRound(static_cast(real_time) / (time_base_dbl_ * 1000)); - SetTime(start_timestamp_ + frames_since_start); + int64_t current_time = start_timestamp_ + frames_since_start * playback_speed_; + + if (current_time < 0) { + current_time = 0; + Pause(); + } + + SetTime(current_time); } void ViewerWidget::ViewerNodeChangedBetween(const rational &start, const rational &end) diff --git a/app/widget/viewer/viewer.h b/app/widget/viewer/viewer.h index 899e9a8e4..ffebcaa14 100644 --- a/app/widget/viewer/viewer.h +++ b/app/widget/viewer/viewer.h @@ -86,6 +86,12 @@ public slots: void GoToEnd(); + void ShuttleLeft(); + + void ShuttleStop(); + + void ShuttleRight(); + signals: void TimeChanged(const rational&); @@ -97,6 +103,8 @@ private: void UpdateTextureFromNode(const rational &time); + void PlayInternal(int speed); + ViewerGLWidget* gl_widget_; PlaybackControls* controls_; @@ -116,6 +124,8 @@ private: ViewerOutput* viewer_node_; + int playback_speed_; + private slots: void RulerTimeChange(int64_t); diff --git a/app/window/mainwindow/mainmenu.cpp b/app/window/mainwindow/mainmenu.cpp index 4b6f55288..b8b36bf7f 100644 --- a/app/window/mainwindow/mainmenu.cpp +++ b/app/window/mainwindow/mainmenu.cpp @@ -23,6 +23,7 @@ #include #include "core.h" +#include "dialog/actionsearch/actionsearch.h" #include "panel/panelmanager.h" #include "tool/tool.h" #include "ui/style/style.h" @@ -169,8 +170,8 @@ MainMenu::MainMenu(QMainWindow *parent) : playback_menu_->addSeparator(); - playback_prevcut_item_ = playback_menu_->AddItem("prevcut", nullptr, nullptr, "Up"); - playback_nextcut_item_ = playback_menu_->AddItem("nextcut", nullptr, nullptr, "Down"); + playback_prevcut_item_ = playback_menu_->AddItem("prevcut", this, SLOT(GoToPrevCutTriggered()), "Up"); + playback_nextcut_item_ = playback_menu_->AddItem("nextcut", this, SLOT(GoToNextCutTriggered()), "Down"); playback_menu_->addSeparator(); @@ -179,9 +180,9 @@ MainMenu::MainMenu(QMainWindow *parent) : playback_menu_->addSeparator(); - playback_shuttleleft_item_ = playback_menu_->AddItem("decspeed", nullptr, nullptr, "J"); - playback_shuttlestop_item_ = playback_menu_->AddItem("pause", nullptr, nullptr, "K"); - playback_shuttleright_item_ = playback_menu_->AddItem("incspeed", nullptr, nullptr, "L"); + playback_shuttleleft_item_ = playback_menu_->AddItem("decspeed", this, SLOT(ShuttleLeftTriggered()), "J"); + playback_shuttlestop_item_ = playback_menu_->AddItem("pause", this, SLOT(ShuttleStopTriggered()), "K"); + playback_shuttleright_item_ = playback_menu_->AddItem("incspeed", this, SLOT(ShuttleRightTriggered()), "L"); playback_menu_->addSeparator(); @@ -193,7 +194,7 @@ MainMenu::MainMenu(QMainWindow *parent) : // window_menu_ = new Menu(this, this, SLOT(WindowMenuAboutToShow())); window_menu_separator_ = window_menu_->addSeparator(); - window_maximize_panel_item_ = window_menu_->AddItem("maximizepanel", nullptr, nullptr, "`"); + window_maximize_panel_item_ = window_menu_->AddItem("maximizepanel", parent, SLOT(ToggleMaximizedPanel()), "`"); window_lock_layout_item_ = window_menu_->AddItem("lockpanels", olive::panel_manager, SLOT(SetPanelsLocked(bool))); window_lock_layout_item_->setCheckable(true); window_menu_->addSeparator(); @@ -294,11 +295,11 @@ MainMenu::MainMenu(QMainWindow *parent) : // HELP MENU // help_menu_ = new Menu(this); - help_action_search_item_ = help_menu_->AddItem("actionsearch", nullptr, nullptr, "/"); + help_action_search_item_ = help_menu_->AddItem("actionsearch", this, SLOT(ActionSearchTriggered()), "/"); help_menu_->addSeparator(); help_debug_log_item_ = help_menu_->AddItem("debuglog", nullptr, nullptr); help_menu_->addSeparator(); - help_about_item_ = help_menu_->AddItem("about", nullptr, nullptr); + help_about_item_ = help_menu_->AddItem("about", &olive::core, SLOT(DialogAboutShow())); Retranslate(); } @@ -427,6 +428,38 @@ void MainMenu::EditToOutTriggered() olive::panel_manager->CurrentlyFocused()->EditToOut(); } +void MainMenu::ActionSearchTriggered() +{ + ActionSearch as(parentWidget()); + as.SetMenuBar(this); + as.exec(); +} + +void MainMenu::ShuttleLeftTriggered() +{ + olive::panel_manager->CurrentlyFocused()->ShuttleLeft(); +} + +void MainMenu::ShuttleStopTriggered() +{ + olive::panel_manager->CurrentlyFocused()->ShuttleStop(); +} + +void MainMenu::ShuttleRightTriggered() +{ + olive::panel_manager->CurrentlyFocused()->ShuttleRight(); +} + +void MainMenu::GoToPrevCutTriggered() +{ + olive::panel_manager->CurrentlyFocused()->GoToPrevCut(); +} + +void MainMenu::GoToNextCutTriggered() +{ + olive::panel_manager->CurrentlyFocused()->GoToNextCut(); +} + void MainMenu::Retranslate() { // MenuShared is not a QWidget and therefore does not receive a LanguageEvent, we use MainMenu's to update it diff --git a/app/window/mainwindow/mainmenu.h b/app/window/mainwindow/mainmenu.h index e343abc57..81371219f 100644 --- a/app/window/mainwindow/mainmenu.h +++ b/app/window/mainwindow/mainmenu.h @@ -24,6 +24,7 @@ #include #include +#include "dialog/actionsearch/actionsearch.h" #include "widget/menu/menu.h" /** @@ -106,6 +107,15 @@ private slots: void EditToInTriggered(); void EditToOutTriggered(); + void ActionSearchTriggered(); + + void ShuttleLeftTriggered(); + void ShuttleStopTriggered(); + void ShuttleRightTriggered(); + + void GoToPrevCutTriggered(); + void GoToNextCutTriggered(); + private: /** * @brief Set strings based on the current application language. @@ -201,6 +211,7 @@ private: QAction* help_action_search_item_; QAction* help_debug_log_item_; QAction* help_about_item_; + }; #endif // MAINMENU_H diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 5f154e308..37bf23750 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -64,6 +64,37 @@ void olive::MainWindow::SetFullscreen(bool fullscreen) } } +void olive::MainWindow::ToggleMaximizedPanel() +{ + qDebug() << "Hello!"; + + if (premaximized_state_.isEmpty()) { + // Assume nothing is maximized at the moment + + // Find the currently focused panel + PanelWidget* currently_focused = olive::panel_manager->CurrentlyFocused(); + + // If this panel is not actually on the main window, this is a no-op + if (currently_focused->isFloating()) { + return; + } + + // Save the current state so it can be restored later + premaximized_state_ = saveState(); + + // For every other panel that is on the main window, hide it + foreach (PanelWidget* panel, olive::panel_manager->panels()) { + if (!panel->isFloating() && panel != currently_focused) { + panel->setVisible(false); + } + } + } else { + // Assume we are currently maximized, restore the state + restoreState(premaximized_state_); + premaximized_state_.clear(); + } +} + void olive::MainWindow::ProjectOpen(Project* p) { // FIXME Use settings data to create panels and restore state if they exist diff --git a/app/window/mainwindow/mainwindow.h b/app/window/mainwindow/mainwindow.h index f852737f2..3e885b2e6 100644 --- a/app/window/mainwindow/mainwindow.h +++ b/app/window/mainwindow/mainwindow.h @@ -38,12 +38,13 @@ public: public slots: void ProjectOpen(Project *p); void SetFullscreen(bool fullscreen); + void ToggleMaximizedPanel(); protected: virtual void closeEvent(QCloseEvent* e) override; private: - Qt::WindowStates old_window_state_; + QByteArray premaximized_state_; };