/*** 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 namespace olive { 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; i < menus.size(); i++) { QMenu *menu = menus.at(i)->menu(); 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; i < actions.size(); i++) { QAction *a = actions.at(i); // Ignore separator actions if (!a->isSeparator()) { 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( QStringLiteral("%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; i < lim; i++) { if (list_widget->item(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; i < lim; i++) { if (list_widget->item(i)->isSelected()) { list_widget->item(i + 1)->setSelected(true); list_widget->scrollToItem(list_widget->item(i + 1)); break; } } } ActionSearchEntry::ActionSearchEntry(QWidget *parent) : QLineEdit(parent) { } bool ActionSearchEntry::event(QEvent *e) { switch (e->type()) { case QEvent::ShortcutOverride: switch (static_cast(e)->key()) { case Qt::Key_Up: case Qt::Key_Down: e->accept(); return true; } break; case QEvent::KeyPress: // Listen for up/down, otherwise pass the key event to the base class. switch (static_cast(e)->key()) { case Qt::Key_Up: e->accept(); emit moveSelectionUp(); return true; case Qt::Key_Down: e->accept(); emit moveSelectionDown(); return true; } break; default: break; } return QLineEdit::event(e); } ActionSearchList::ActionSearchList(QWidget *parent) : QListWidget(parent) { } void ActionSearchList::mouseDoubleClickEvent(QMouseEvent *) { // Indiscriminately emit a signal on any double click emit dbl_click(); } }