Files
oak-editor/app/dialog/actionsearch/actionsearch.cpp
T

276 lines
8.3 KiB
C++

/***
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 <http://www.gnu.org/licenses/>.
***/
#include "actionsearch.h"
#include <QKeyEvent>
#include <QLabel>
#include <QMenuBar>
#include <QVBoxLayout>
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<QAction *> 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<QAction *> 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<quintptr>(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<QListWidgetItem *> 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<QAction *>(
item->data(Qt::UserRole + 1).value<quintptr>());
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<QKeyEvent *>(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<QKeyEvent *>(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();
}
}