From b3ef50350fa9ee370ca5fecc342a5ad9a67b1e94 Mon Sep 17 00:00:00 2001 From: askmeaboutloom Date: Wed, 18 Sep 2024 09:25:14 +0200 Subject: [PATCH] Allow assigning brush presets to shortcuts And also rejigging a bunch of other stuff with regards to shortcuts and the settings dialog. The input tab is now the tablet tab instead and there's a button at the bottom for the tablet tester. Analogously, there's a touch tester button on the touch page. Brush presets are their own tab in the shortcuts settings and allow assigning the same key sequence to multiple brushes, which it will toggle through in sequence. Conflicts are handled across all types of shortcuts now and you can filter for them. The mirror and flip actions have gotten extra descriptive text to allow searching for them as "mirror" and "flip", since otherwise you may think you can only do one of those. Searching for the keybinds themselves is also possible now. Implements #1367, #1368 and #1377. Relates to #1386. --- src/desktop/CMakeLists.txt | 8 +- .../theme/dark/dialog-input-devices.svg | 12 - .../assets/theme/dark/input-tablet.svg | 14 + .../assets/theme/dark/input-touchscreen.svg | 12 + .../theme/light/dialog-input-devices.svg | 12 - .../assets/theme/light/input-tablet.svg | 14 + .../assets/theme/light/input-touchscreen.svg | 12 + src/desktop/dialogs/brushsettingsdialog.cpp | 77 ++- src/desktop/dialogs/brushsettingsdialog.h | 9 +- src/desktop/dialogs/settingsdialog.cpp | 72 ++- src/desktop/dialogs/settingsdialog.h | 15 +- .../settingsdialog/proportionaltableview.cpp | 22 +- .../settingsdialog/proportionaltableview.h | 12 +- .../settingsdialog/shortcutfilterinput.cpp | 71 +++ .../settingsdialog/shortcutfilterinput.h | 38 ++ .../dialogs/settingsdialog/shortcuts.cpp | 458 +++++++++++------- .../dialogs/settingsdialog/shortcuts.h | 44 +- .../settingsdialog/{input.cpp => tablet.cpp} | 138 +++--- .../settingsdialog/{input.h => tablet.h} | 24 +- src/desktop/dialogs/settingsdialog/touch.cpp | 171 +++++++ src/desktop/dialogs/settingsdialog/touch.h | 52 ++ src/desktop/dialogs/startdialog/links.cpp | 2 +- src/desktop/docks/brushpalettedock.cpp | 20 + src/desktop/docks/brushpalettedock.h | 2 + src/desktop/mainwindow.cpp | 290 ++++++++--- src/desktop/mainwindow.h | 11 + src/desktop/toolwidgets/brushsettings.cpp | 20 + src/desktop/toolwidgets/brushsettings.h | 3 + src/desktop/update-assets-icons.sh | 2 +- src/desktop/utils/actionbuilder.h | 13 +- src/libclient/CMakeLists.txt | 2 + src/libclient/brushes/brushpresetmodel.cpp | 395 +++++++++++++-- src/libclient/brushes/brushpresetmodel.h | 33 +- src/libclient/canvas/canvasshortcuts.cpp | 31 +- src/libclient/canvas/canvasshortcuts.h | 4 +- src/libclient/utils/brushshortcutmodel.cpp | 277 +++++++++++ src/libclient/utils/brushshortcutmodel.h | 84 ++++ src/libclient/utils/canvasshortcutsmodel.cpp | 153 ++++-- src/libclient/utils/canvasshortcutsmodel.h | 39 +- src/libclient/utils/customshortcutmodel.cpp | 183 +++++-- src/libclient/utils/customshortcutmodel.h | 40 +- 41 files changed, 2346 insertions(+), 545 deletions(-) delete mode 100644 src/desktop/assets/theme/dark/dialog-input-devices.svg create mode 100644 src/desktop/assets/theme/dark/input-tablet.svg create mode 100644 src/desktop/assets/theme/dark/input-touchscreen.svg delete mode 100644 src/desktop/assets/theme/light/dialog-input-devices.svg create mode 100644 src/desktop/assets/theme/light/input-tablet.svg create mode 100644 src/desktop/assets/theme/light/input-touchscreen.svg create mode 100644 src/desktop/dialogs/settingsdialog/shortcutfilterinput.cpp create mode 100644 src/desktop/dialogs/settingsdialog/shortcutfilterinput.h rename src/desktop/dialogs/settingsdialog/{input.cpp => tablet.cpp} (60%) rename src/desktop/dialogs/settingsdialog/{input.h => tablet.h} (50%) create mode 100644 src/desktop/dialogs/settingsdialog/touch.cpp create mode 100644 src/desktop/dialogs/settingsdialog/touch.h create mode 100644 src/libclient/utils/brushshortcutmodel.cpp create mode 100644 src/libclient/utils/brushshortcutmodel.h diff --git a/src/desktop/CMakeLists.txt b/src/desktop/CMakeLists.txt index a4f0c89b7..86b32ccc6 100644 --- a/src/desktop/CMakeLists.txt +++ b/src/desktop/CMakeLists.txt @@ -135,8 +135,6 @@ target_sources(drawpile PRIVATE dialogs/settingsdialog/general.cpp dialogs/settingsdialog/general.h dialogs/settingsdialog/helpers.h - dialogs/settingsdialog/input.cpp - dialogs/settingsdialog/input.h dialogs/settingsdialog/network.cpp dialogs/settingsdialog/network.h dialogs/settingsdialog/notifications.cpp @@ -151,8 +149,14 @@ target_sources(drawpile PRIVATE dialogs/settingsdialog/userinterface.h dialogs/settingsdialog/servers.cpp dialogs/settingsdialog/servers.h + dialogs/settingsdialog/shortcutfilterinput.cpp + dialogs/settingsdialog/shortcutfilterinput.h dialogs/settingsdialog/shortcuts.cpp dialogs/settingsdialog/shortcuts.h + dialogs/settingsdialog/tablet.cpp + dialogs/settingsdialog/tablet.h + dialogs/settingsdialog/touch.cpp + dialogs/settingsdialog/touch.h dialogs/settingsdialog/tools.cpp dialogs/settingsdialog/tools.h dialogs/startdialog.cpp diff --git a/src/desktop/assets/theme/dark/dialog-input-devices.svg b/src/desktop/assets/theme/dark/dialog-input-devices.svg deleted file mode 100644 index 1d9aa656b..000000000 --- a/src/desktop/assets/theme/dark/dialog-input-devices.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/src/desktop/assets/theme/dark/input-tablet.svg b/src/desktop/assets/theme/dark/input-tablet.svg new file mode 100644 index 000000000..0b2be556a --- /dev/null +++ b/src/desktop/assets/theme/dark/input-tablet.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/src/desktop/assets/theme/dark/input-touchscreen.svg b/src/desktop/assets/theme/dark/input-touchscreen.svg new file mode 100644 index 000000000..670bb56ba --- /dev/null +++ b/src/desktop/assets/theme/dark/input-touchscreen.svg @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/src/desktop/assets/theme/light/dialog-input-devices.svg b/src/desktop/assets/theme/light/dialog-input-devices.svg deleted file mode 100644 index 21a7f983c..000000000 --- a/src/desktop/assets/theme/light/dialog-input-devices.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/src/desktop/assets/theme/light/input-tablet.svg b/src/desktop/assets/theme/light/input-tablet.svg new file mode 100644 index 000000000..79e3d5ac2 --- /dev/null +++ b/src/desktop/assets/theme/light/input-tablet.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/src/desktop/assets/theme/light/input-touchscreen.svg b/src/desktop/assets/theme/light/input-touchscreen.svg new file mode 100644 index 000000000..41fe3a20d --- /dev/null +++ b/src/desktop/assets/theme/light/input-touchscreen.svg @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/src/desktop/dialogs/brushsettingsdialog.cpp b/src/desktop/dialogs/brushsettingsdialog.cpp index b050bf184..5ddd4f7fe 100644 --- a/src/desktop/dialogs/brushsettingsdialog.cpp +++ b/src/desktop/dialogs/brushsettingsdialog.cpp @@ -3,6 +3,7 @@ #include "desktop/filewrangler.h" #include "desktop/utils/widgetutils.h" #include "desktop/widgets/curvewidget.h" +#include "desktop/widgets/keysequenceedit.h" #include "desktop/widgets/kis_slider_spin_box.h" #include "desktop/widgets/toolmessage.h" #include "libclient/canvas/blendmodes.h" @@ -30,6 +31,33 @@ namespace dialogs { +namespace { + +class ShortcutLineEdit : public QLineEdit { +public: + ShortcutLineEdit(QPushButton *button) + : QLineEdit() + , m_button(button) + { + setReadOnly(true); + setEnabled(false); + } + +protected: + void mousePressEvent(QMouseEvent *event) override + { + QLineEdit::mousePressEvent(event); + if(event->button() == Qt::LeftButton) { + m_button->click(); + } + } + +private: + QPushButton *m_button; +}; + +} + struct BrushSettingsDialog::Private { struct MyPaintPage { KisDoubleSliderSpinBox *baseValueSpinner; @@ -51,6 +79,8 @@ struct BrushSettingsDialog::Private { QLineEdit *presetLabelEdit; QLineEdit *presetNameEdit; QPlainTextEdit *presetDescriptionEdit; + ShortcutLineEdit *presetShortcutEdit; + QPushButton *presetShortcutButton; QComboBox *brushTypeCombo; QLabel *brushModeLabel; QComboBox *brushModeCombo; @@ -94,6 +124,7 @@ struct BrushSettingsDialog::Private { DP_BrushShape lastShape; brushes::ActiveBrush brush; int globalSmoothing; + int presetId = 0; bool useBrushSampleCount; bool presetAttached = true; bool updating = false; @@ -129,7 +160,18 @@ void BrushSettingsDialog::showGeneralPage() } -void BrushSettingsDialog::setPresetAttached(bool presetAttached) +bool BrushSettingsDialog::isPresetAttached() const +{ + return d->presetAttached; +} + +int BrushSettingsDialog::presetId() const +{ + return d->presetId; +} + + +void BrushSettingsDialog::setPresetAttached(bool presetAttached, int presetId) { utils::ScopedUpdateDisabler disabler(this); if(presetAttached && !d->presetAttached) { @@ -139,6 +181,7 @@ void BrushSettingsDialog::setPresetAttached(bool presetAttached) d->presetAttachedWidget->hide(); d->presetDetachedWidget->show(); } + d->presetId = presetId; d->presetAttached = presetAttached; d->presetAttachedWidget->setEnabled(presetAttached); d->overwriteBrushButton->setEnabled(presetAttached); @@ -171,6 +214,13 @@ void BrushSettingsDialog::setPresetThumbnail(const QPixmap &presetThumbnail) } } +void BrushSettingsDialog::setPresetShortcut(const QKeySequence &presetShortcut) +{ + QString text = presetShortcut.toString(QKeySequence::NativeText); + d->presetShortcutEdit->setText( + text.isEmpty() ? tr("No shortcut assigned") : text); +} + void BrushSettingsDialog::setForceEraseMode(bool forceEraseMode) { d->eraseModeBox->setEnabled(!forceEraseMode); @@ -343,6 +393,24 @@ QWidget *BrushSettingsDialog::buildPresetPageUi() attachedLayout->setContentsMargins(0, 0, 0, 0); d->presetAttachedWidget->setLayout(attachedLayout); + d->presetShortcutButton = new QPushButton(tr("Change…")); + connect( + d->presetShortcutButton, &QPushButton::clicked, this, + &BrushSettingsDialog::requestShortcutChange); + + d->presetShortcutEdit = new ShortcutLineEdit(d->presetShortcutButton); + d->presetShortcutEdit->setAlignment(Qt::AlignCenter); + d->presetShortcutEdit->setReadOnly(true); + d->presetShortcutEdit->setEnabled(false); + + QHBoxLayout *shortcutLayout = new QHBoxLayout; + shortcutLayout->setContentsMargins(0, 0, 0, 0); + shortcutLayout->addWidget(d->presetShortcutEdit); + shortcutLayout->addWidget(d->presetShortcutButton); + attachedLayout->addRow(tr("Shortcut:"), shortcutLayout); + + utils::addFormSpacer(attachedLayout); + QGridLayout *thumbnailLayout = new QGridLayout; d->presetThumbnailView = new QGraphicsView; @@ -1339,6 +1407,13 @@ bool BrushSettingsDialog::disableIndirectMyPaintInputs(int setting) } } +void BrushSettingsDialog::requestShortcutChange() +{ + if(d->presetAttached) { + emit shortcutChangeRequested(d->presetId); + } +} + void BrushSettingsDialog::choosePresetThumbnailFile() { FileWrangler::ImageOpenFn imageOpenCompleted = [this](QImage &img) { diff --git a/src/desktop/dialogs/brushsettingsdialog.h b/src/desktop/dialogs/brushsettingsdialog.h index 5b16c5b72..fa1d7709a 100644 --- a/src/desktop/dialogs/brushsettingsdialog.h +++ b/src/desktop/dialogs/brushsettingsdialog.h @@ -8,6 +8,7 @@ class KisSliderSpinBox; class QComboBox; +class QKeySequence; class QListWidgetItem; class QPushButton; class QVBoxLayout; @@ -23,6 +24,9 @@ class BrushSettingsDialog final : public QDialog { void showPresetPage(); void showGeneralPage(); + bool isPresetAttached() const; + int presetId() const; + signals: void presetNameChanged(const QString &presetName); void presetDescriptionChanged(const QString &presetDescription); @@ -30,12 +34,14 @@ class BrushSettingsDialog final : public QDialog { void brushSettingsChanged(const brushes::ActiveBrush &brush); void newBrushRequested(); void overwriteBrushRequested(); + void shortcutChangeRequested(int presetId); public slots: - void setPresetAttached(bool presetAttached); + void setPresetAttached(bool presetAttached, int presetId); void setPresetName(const QString &presetName); void setPresetDescription(const QString &presetDescription); void setPresetThumbnail(const QPixmap &presetThumbnail); + void setPresetShortcut(const QKeySequence &presetShortcut); void setForceEraseMode(bool forceEraseMode); void setStabilizerUseBrushSampleCount(bool useBrushSampleCount); void setGlobalSmoothing(int smoothing); @@ -121,6 +127,7 @@ private slots: static QString getMyPaintSettingTitle(int setting); static QString getMyPaintSettingDescription(int setting); + void requestShortcutChange(); void choosePresetThumbnailFile(); void showPresetThumbnail(const QPixmap &thumbnail); void renderPresetThumbnail(); diff --git a/src/desktop/dialogs/settingsdialog.cpp b/src/desktop/dialogs/settingsdialog.cpp index ac35b7fdd..306bf74a8 100644 --- a/src/desktop/dialogs/settingsdialog.cpp +++ b/src/desktop/dialogs/settingsdialog.cpp @@ -1,13 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "desktop/dialogs/settingsdialog.h" #include "desktop/dialogs/settingsdialog/general.h" -#include "desktop/dialogs/settingsdialog/input.h" #include "desktop/dialogs/settingsdialog/network.h" #include "desktop/dialogs/settingsdialog/notifications.h" #include "desktop/dialogs/settingsdialog/parentalcontrols.h" #include "desktop/dialogs/settingsdialog/servers.h" #include "desktop/dialogs/settingsdialog/shortcuts.h" +#include "desktop/dialogs/settingsdialog/tablet.h" #include "desktop/dialogs/settingsdialog/tools.h" +#include "desktop/dialogs/settingsdialog/touch.h" #include "desktop/dialogs/settingsdialog/userinterface.h" #include "desktop/main.h" #include "desktop/settings.h" @@ -16,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -79,6 +81,8 @@ SettingsDialog::SettingsDialog( #else bool showServers = true; #endif + settingsdialog::Tablet *tabletPage; + settingsdialog::Touch *touchPage; const std::initializer_list< std::tuple> panels = { @@ -86,8 +90,10 @@ SettingsDialog::SettingsDialog( new settingsdialog::General(m_settings, this), true}, {"window_", tr("User Interface"), new settingsdialog::UserInterface(m_settings, this), true}, - {"dialog-input-devices", tr("Input"), - new settingsdialog::Input(m_settings, this), true}, + {"input-tablet", tr("Tablet"), + (tabletPage = new settingsdialog::Tablet(m_settings, this)), true}, + {"input-touchscreen", tr("Touch"), + (touchPage = new settingsdialog::Touch(m_settings, this)), true}, {"tools", tr("Tools"), new settingsdialog::Tools(m_settings, this), true}, {"network-modem", tr("Network"), @@ -97,9 +103,7 @@ SettingsDialog::SettingsDialog( {"network-server-database", tr("Servers"), new settingsdialog::Servers(m_settings, singleSession, this), showServers}, - //: This refers to keyboard, canvas and touch shortcuts. That means - //: shortcuts don't necessarily involve keys! - {"favorite", tr("Shortcuts", "action"), + {"input-keyboard", tr("Shortcuts"), new settingsdialog::Shortcuts(m_settings, this), true}, {"flag", tr("Parental Controls"), new settingsdialog::ParentalControls(m_settings, this), true}}; @@ -157,10 +161,10 @@ SettingsDialog::SettingsDialog( button->setProperty("panel", QVariant::fromValue(panel)); connect( button, &QToolButton::toggled, this, - [=, panelToActivate = panel](bool checked) { + [this, buttons, panelToActivate = panel](bool checked) { if(checked) { setUpdatesEnabled(false); - activatePanel(panelToActivate); + activatePanel(panelToActivate, buttons); setUpdatesEnabled(true); } }); @@ -168,6 +172,15 @@ SettingsDialog::SettingsDialog( first = false; } + tabletPage->createButtons(buttons); + touchPage->createButtons(buttons); + connect( + tabletPage, &settingsdialog::Tablet::tabletTesterRequested, this, + &SettingsDialog::tabletTesterRequested); + connect( + touchPage, &settingsdialog::Touch::touchTesterRequested, this, + &SettingsDialog::touchTesterRequested); + menuLayout->addStretch(); QBoxLayout *layout = new QBoxLayout( @@ -184,19 +197,56 @@ SettingsDialog::~SettingsDialog() m_settings.scalingSettings()->sync(); } -void SettingsDialog::activateShortcutsPanel() +void SettingsDialog::initiateFixShortcutConflicts() +{ + settingsdialog::Shortcuts *shortcuts = activateShortcutsPanel(); + if(shortcuts) { + shortcuts->initiateFixShortcutConflicts(); + } +} + +void SettingsDialog::initiateBrushShortcutChange(int presetId) +{ + settingsdialog::Shortcuts *shortcuts = activateShortcutsPanel(); + if(shortcuts) { + shortcuts->initiateBrushShortcutChange(presetId); + } +} + +settingsdialog::Shortcuts *SettingsDialog::activateShortcutsPanel() { for(QAbstractButton *button : m_group->buttons()) { QWidget *panel = button->property("panel").value(); - if(qobject_cast(panel)) { + settingsdialog::Shortcuts *shortcuts = + qobject_cast(panel); + if(shortcuts) { button->click(); + return shortcuts; } } + return nullptr; } -void SettingsDialog::activatePanel(QWidget *panel) +void SettingsDialog::activatePanel(QWidget *panel, QDialogButtonBox *buttons) { + QAbstractButton *closeButton = buttons->button(QDialogButtonBox::Close); + for(QAbstractButton *button : buttons->buttons()) { + if(button != closeButton) { + button->hide(); + } + } m_stack->setCurrentWidget(panel); + + settingsdialog::Tablet *tablet = + qobject_cast(panel); + if(tablet) { + tablet->showButtons(); + } + + settingsdialog::Touch *touch = qobject_cast(panel); + if(touch) { + touch->showButtons(); + } } void SettingsDialog::addPanel(QWidget *panel) diff --git a/src/desktop/dialogs/settingsdialog.h b/src/desktop/dialogs/settingsdialog.h index c64a90371..c1fa3d1f1 100644 --- a/src/desktop/dialogs/settingsdialog.h +++ b/src/desktop/dialogs/settingsdialog.h @@ -4,6 +4,7 @@ #include class QButtonGroup; +class QDialogButtonBox; class QStackedWidget; namespace desktop { @@ -14,6 +15,10 @@ class Settings; namespace dialogs { +namespace settingsdialog { +class Shortcuts; +} + class SettingsDialog final : public QDialog { Q_OBJECT public: @@ -21,10 +26,16 @@ class SettingsDialog final : public QDialog { bool singleSession, bool smallScreenMode, QWidget *parent = nullptr); ~SettingsDialog() override; - void activateShortcutsPanel(); + void initiateFixShortcutConflicts(); + void initiateBrushShortcutChange(int presetId); + +signals: + void tabletTesterRequested(); + void touchTesterRequested(); private: - void activatePanel(QWidget *panel); + settingsdialog::Shortcuts *activateShortcutsPanel(); + void activatePanel(QWidget *panel, QDialogButtonBox *buttons); void addPanel(QWidget *panel); desktop::settings::Settings &m_settings; diff --git a/src/desktop/dialogs/settingsdialog/proportionaltableview.cpp b/src/desktop/dialogs/settingsdialog/proportionaltableview.cpp index d2f55d864..9698faa87 100644 --- a/src/desktop/dialogs/settingsdialog/proportionaltableview.cpp +++ b/src/desktop/dialogs/settingsdialog/proportionaltableview.cpp @@ -1,9 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #include "desktop/dialogs/settingsdialog/proportionaltableview.h" - +#include "desktop/dialogs/settingsdialog/shortcutfilterinput.h" #include -#include #include namespace dialogs { @@ -39,7 +37,8 @@ void ProportionalTableView::resizeEvent(QResizeEvent *event) } ProportionalTableView *ProportionalTableView::make( - QLineEdit *filter, QAbstractItemModel *model) + ShortcutFilterInput *filter, int filterRole, QAbstractItemModel *model, + QSortFilterProxyModel *filterModel, bool connectFilter) { auto *view = new ProportionalTableView; view->setCornerButtonEnabled(false); @@ -51,12 +50,19 @@ ProportionalTableView *ProportionalTableView::make( view->setWordWrap(false); view->setEditTriggers(QAbstractItemView::AllEditTriggers); - auto *filterModel = new QSortFilterProxyModel(view); + if(filterModel) { + filterModel->setParent(view); + } else { + filterModel = new QSortFilterProxyModel(view); + } + filterModel->setFilterRole(filterRole); filterModel->setSourceModel(model); filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); - QObject::connect( - filter, &QLineEdit::textChanged, filterModel, - &QSortFilterProxyModel::setFilterFixedString); + if(connectFilter) { + QObject::connect( + filter, &ShortcutFilterInput::filtered, filterModel, + &QSortFilterProxyModel::setFilterFixedString); + } view->setModel(filterModel); view->sortByColumn(0, Qt::AscendingOrder); diff --git a/src/desktop/dialogs/settingsdialog/proportionaltableview.h b/src/desktop/dialogs/settingsdialog/proportionaltableview.h index 9f792f63f..4eb4df0da 100644 --- a/src/desktop/dialogs/settingsdialog/proportionaltableview.h +++ b/src/desktop/dialogs/settingsdialog/proportionaltableview.h @@ -1,23 +1,25 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef DESKTOP_DIALOGS_SETTINGSDIALOG_PROPORTIONALTABLEVIEW_H #define DESKTOP_DIALOGS_SETTINGSDIALOG_PROPORTIONALTABLEVIEW_H - #include #include class QAbstractItemModel; -class QLineEdit; +class QSortFilterProxyModel; namespace dialogs { namespace settingsdialog { +class ShortcutFilterInput; + class ProportionalTableView final : public QTableView { public: using Stretches = std::array; - static ProportionalTableView * - make(QLineEdit *filter, QAbstractItemModel *model); + static ProportionalTableView *make( + ShortcutFilterInput *filter, int filterRole, QAbstractItemModel *model, + QSortFilterProxyModel *filterModel = nullptr, + bool connectFilter = true); ProportionalTableView(QWidget *parent = nullptr); diff --git a/src/desktop/dialogs/settingsdialog/shortcutfilterinput.cpp b/src/desktop/dialogs/settingsdialog/shortcutfilterinput.cpp new file mode 100644 index 000000000..d970a916e --- /dev/null +++ b/src/desktop/dialogs/settingsdialog/shortcutfilterinput.cpp @@ -0,0 +1,71 @@ +#include "desktop/dialogs/settingsdialog/shortcutfilterinput.h" +#include +#include +#include + +namespace dialogs { +namespace settingsdialog { + +ShortcutFilterInput::ShortcutFilterInput(QWidget *parent) + : QWidget(parent) +{ + QHBoxLayout *layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + setContentsMargins(0, 0, 0, 0); + + m_filterEdit = new QLineEdit; + m_filterEdit->setClearButtonEnabled(true); + m_filterEdit->setPlaceholderText(tr("Search…")); + m_filterEdit->addAction( + QIcon::fromTheme("edit-find"), QLineEdit::LeadingPosition); + layout->addWidget(m_filterEdit, 1); + + m_conflictBox = new QCheckBox(tr("Show conflicts only")); + layout->addWidget(m_conflictBox); + + connect( + m_filterEdit, &QLineEdit::textChanged, this, + &ShortcutFilterInput::handleFilterTextChanged); + connect( + m_conflictBox, &QCheckBox::stateChanged, this, + &ShortcutFilterInput::handleConflictBoxStateChanged); +} + +bool ShortcutFilterInput::isEmpty() const +{ + return m_filterText.isEmpty(); +} + +void ShortcutFilterInput::checkConflictBox() +{ + if(!m_conflictBox->isChecked()) { + m_conflictBox->click(); + } +} + +void ShortcutFilterInput::handleFilterTextChanged(const QString &text) +{ + if(!m_conflictBox->isChecked()) { + updateFilterText(text); + } +} + +void ShortcutFilterInput::handleConflictBoxStateChanged(int state) +{ + bool checked = state != Qt::Unchecked; + m_filterEdit->setDisabled(checked); + updateFilterText(checked ? QStringLiteral("\1") : m_filterEdit->text()); + emit conflictBoxChecked(checked); +} + +void ShortcutFilterInput::updateFilterText(const QString &text) +{ + QString filterText = text.trimmed(); + if(m_filterText != filterText) { + m_filterText = filterText; + emit filtered(m_filterText); + } +} + +} +} diff --git a/src/desktop/dialogs/settingsdialog/shortcutfilterinput.h b/src/desktop/dialogs/settingsdialog/shortcutfilterinput.h new file mode 100644 index 000000000..4d6663730 --- /dev/null +++ b/src/desktop/dialogs/settingsdialog/shortcutfilterinput.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#ifndef DESKTOP_DIALOGS_SETTINGSDIALOG_SHORTCUTFILTERINPUT_H +#define DESKTOP_DIALOGS_SETTINGSDIALOG_SHORTCUTFILTERINPUT_H +#include + +class QCheckBox; +class QLineEdit; + +namespace dialogs { +namespace settingsdialog { + +class ShortcutFilterInput : public QWidget { + Q_OBJECT +public: + ShortcutFilterInput(QWidget *parent = nullptr); + + bool isEmpty() const; + + void checkConflictBox(); + +signals: + void filtered(const QString &text); + void conflictBoxChecked(bool checked); + +private: + void handleFilterTextChanged(const QString &text); + void handleConflictBoxStateChanged(int state); + void updateFilterText(const QString &text); + + QLineEdit *m_filterEdit; + QCheckBox *m_conflictBox; + QString m_filterText; +}; + +} +} + +#endif diff --git a/src/desktop/dialogs/settingsdialog/shortcuts.cpp b/src/desktop/dialogs/settingsdialog/shortcuts.cpp index 3fdf55cbe..e03fdd31f 100644 --- a/src/desktop/dialogs/settingsdialog/shortcuts.cpp +++ b/src/desktop/dialogs/settingsdialog/shortcuts.cpp @@ -3,57 +3,44 @@ #include "desktop/dialogs/canvasshortcutsdialog.h" #include "desktop/dialogs/settingsdialog/helpers.h" #include "desktop/dialogs/settingsdialog/proportionaltableview.h" +#include "desktop/dialogs/settingsdialog/shortcutfilterinput.h" #include "desktop/main.h" #include "desktop/settings.h" #include "desktop/utils/widgetutils.h" #include "desktop/widgets/groupedtoolbutton.h" #include "desktop/widgets/keysequenceedit.h" +#include "libclient/brushes/brushpresetmodel.h" +#include "libclient/utils/brushshortcutmodel.h" #include "libclient/utils/canvasshortcutsmodel.h" #include "libclient/utils/customshortcutmodel.h" #include "libshared/util/qtcompat.h" #include +#include #include #include #include #include #include #include +#include #include #include #include #include #include +#define ON_ANY_MODEL_CHANGE(MODEL, RECEIVER, FN) \ + do { \ + connect(MODEL, &QAbstractItemModel::dataChanged, RECEIVER, FN); \ + connect(MODEL, &QAbstractItemModel::modelReset, RECEIVER, FN); \ + connect(MODEL, &QAbstractItemModel::rowsInserted, RECEIVER, FN); \ + connect(MODEL, &QAbstractItemModel::rowsRemoved, RECEIVER, FN); \ + } while(0) + namespace dialogs { namespace settingsdialog { -Shortcuts::Shortcuts(desktop::settings::Settings &settings, QWidget *parent) - : QWidget(parent) -{ - QVBoxLayout *layout = new QVBoxLayout(this); - QLineEdit *filter = new QLineEdit; - filter->setClearButtonEnabled(true); - filter->setPlaceholderText(tr("Search actions…")); - filter->addAction( - QIcon::fromTheme("edit-find"), QLineEdit::LeadingPosition); - layout->addWidget(filter); - - m_tabs = new QTabWidget; - m_tabs->addTab( - initActionShortcuts(settings, filter), - QIcon::fromTheme("input-keyboard"), tr("Actions")); - m_tabs->addTab( - initCanvasShortcuts(settings, filter), QIcon::fromTheme("edit-image"), - tr("Canvas")); - m_tabs->addTab( - initTouchShortcuts(settings), QIcon::fromTheme("hand"), tr("Touch")); - layout->addWidget(m_tabs); -} - -void Shortcuts::finishEditing() -{ - m_shortcutsTable->setCurrentIndex(QModelIndex{}); -} +namespace { class KeySequenceEditFactory final : public QItemEditorCreatorBase { public: @@ -80,8 +67,93 @@ class KeySequenceEditFactory final : public QItemEditorCreatorBase { Shortcuts *m_shortcuts; }; +} + +Shortcuts::Shortcuts(desktop::settings::Settings &settings, QWidget *parent) + : QWidget(parent) + , m_shortcutConflictDebounce(100) +{ + QVBoxLayout *layout = new QVBoxLayout(this); + m_filter = new ShortcutFilterInput; + layout->addWidget(m_filter); + + QStyledItemDelegate *keySequenceDelegate = new QStyledItemDelegate(this); + m_itemEditorFactory.registerEditor( + compat::metaTypeFromName("QKeySequence"), + new KeySequenceEditFactory(this)); + keySequenceDelegate->setItemEditorFactory(&m_itemEditorFactory); + + m_tabs = new QTabWidget; + Q_ASSERT(m_tabs->count() == ACTION_TAB); + m_tabs->addTab( + initActionShortcuts(settings, keySequenceDelegate), + QIcon::fromTheme("input-keyboard"), QString()); + Q_ASSERT(m_tabs->count() == BRUSH_TAB); + m_tabs->addTab( + initBrushShortcuts(keySequenceDelegate), QIcon::fromTheme("draw-brush"), + QString()); + Q_ASSERT(m_tabs->count() == CANVAS_TAB); + m_tabs->addTab( + initCanvasShortcuts(settings), QIcon::fromTheme("edit-image"), + QString()); + layout->addWidget(m_tabs); + + updateConflicts(); + ON_ANY_MODEL_CHANGE( + m_actionShortcutsModel, &m_shortcutConflictDebounce, + &DebounceTimer::setNone); + ON_ANY_MODEL_CHANGE( + m_brushShortcutsModel, &m_shortcutConflictDebounce, + &DebounceTimer::setNone); + ON_ANY_MODEL_CHANGE( + m_canvasShortcutsModel, &m_shortcutConflictDebounce, + &DebounceTimer::setNone); + connect( + &m_shortcutConflictDebounce, &DebounceTimer::noneChanged, this, + &Shortcuts::updateConflicts); + + connect( + m_filter, &ShortcutFilterInput::filtered, this, + &Shortcuts::updateTabTexts, Qt::QueuedConnection); + updateTabTexts(); +} + +void Shortcuts::initiateFixShortcutConflicts() +{ + m_filter->checkConflictBox(); + if(m_actionShortcutsModel->hasConflicts()) { + m_tabs->setCurrentIndex(ACTION_TAB); + } else if(m_brushShortcutsModel->hasConflicts()) { + m_tabs->setCurrentIndex(BRUSH_TAB); + } else if(m_canvasShortcutsModel->hasConflicts()) { + m_tabs->setCurrentIndex(CANVAS_TAB); + } +} + +void Shortcuts::initiateBrushShortcutChange(int presetId) +{ + m_tabs->setCurrentIndex(BRUSH_TAB); + QSortFilterProxyModel *model = + qobject_cast(m_brushesTable->model()); + QModelIndex idx = m_brushShortcutsModel->indexById( + presetId, int(BrushShortcutModel::Shortcut)); + if(model && idx.isValid()) { + QModelIndex mappedIdx = model->mapFromSource(idx); + m_brushesTable->scrollTo(mappedIdx); + m_brushesTable->setCurrentIndex(mappedIdx); + emit m_brushesTable->clicked(mappedIdx); + } +} + +void Shortcuts::finishEditing() +{ + m_actionsTable->setCurrentIndex(QModelIndex()); + m_brushesTable->setCurrentIndex(QModelIndex()); +} + QWidget *Shortcuts::initActionShortcuts( - desktop::settings::Settings &settings, QLineEdit *filter) + desktop::settings::Settings &settings, + QStyledItemDelegate *keySequenceDelegate) { QWidget *widget = new QWidget; QVBoxLayout *layout = new QVBoxLayout(widget); @@ -90,7 +162,7 @@ QWidget *Shortcuts::initActionShortcuts( // This is also a gross way to access this model, but since these models are // not first-class models and I am not rewriting them right now, this is how // it will be in order to get the “Restore defaults” hooked up to everything - m_globalShortcutsModel = shortcutsModel; + m_actionShortcutsModel = shortcutsModel; shortcutsModel->loadShortcuts(settings.shortcuts()); connect( shortcutsModel, &QAbstractItemModel::dataChanged, &settings, @@ -100,19 +172,15 @@ QWidget *Shortcuts::initActionShortcuts( connect( &dpApp(), &DrawpileApp::shortcutsChanged, shortcutsModel, &CustomShortcutModel::updateShortcuts); - m_shortcutsTable = ProportionalTableView::make(filter, shortcutsModel); - m_shortcutsTable->setColumnStretches({6, 2, 2, 2}); - utils::bindKineticScrolling(m_shortcutsTable); + m_actionsTable = ProportionalTableView::make( + m_filter, int(CustomShortcutModel::FilterRole), shortcutsModel); + m_actionsTable->setColumnStretches({6, 2, 2, 2}); + utils::bindKineticScrolling(m_actionsTable); - QStyledItemDelegate *keySequenceDelegate = new QStyledItemDelegate(this); - m_itemEditorFactory.registerEditor( - compat::metaTypeFromName("QKeySequence"), - new KeySequenceEditFactory{this}); - keySequenceDelegate->setItemEditorFactory(&m_itemEditorFactory); - m_shortcutsTable->setItemDelegateForColumn(1, keySequenceDelegate); - m_shortcutsTable->setItemDelegateForColumn(2, keySequenceDelegate); + m_actionsTable->setItemDelegateForColumn(1, keySequenceDelegate); + m_actionsTable->setItemDelegateForColumn(2, keySequenceDelegate); - layout->addWidget(m_shortcutsTable, 1); + layout->addWidget(m_actionsTable, 1); utils::EncapsulatedLayout *actions = new utils::EncapsulatedLayout; actions->setContentsMargins(0, 0, 0, 0); @@ -127,7 +195,7 @@ QWidget *Shortcuts::initActionShortcuts( tr("Really restore all shortcuts to their default values?"), this); if(confirm) { settings.setShortcuts({}); - m_globalShortcutsModel->loadShortcuts({}); + m_actionShortcutsModel->loadShortcuts({}); } }); actions->addWidget(restoreDefaults); @@ -136,14 +204,49 @@ QWidget *Shortcuts::initActionShortcuts( return widget; } -template -static void -onAnyModelChange(QAbstractItemModel *model, QObject *receiver, Fn fn) +QWidget *Shortcuts::initBrushShortcuts(QStyledItemDelegate *keySequenceDelegate) { - QObject::connect(model, &QAbstractItemModel::dataChanged, receiver, fn); - QObject::connect(model, &QAbstractItemModel::modelReset, receiver, fn); - QObject::connect(model, &QAbstractItemModel::rowsInserted, receiver, fn); - QObject::connect(model, &QAbstractItemModel::rowsRemoved, receiver, fn); + int thumbnailSize = brushes::BrushPresetModel::THUMBNAIL_SIZE; + brushes::BrushPresetTagModel *tagModel = dpApp().brushPresets(); + m_brushShortcutsModel = new BrushShortcutModel( + tagModel->presetModel(), QSize(thumbnailSize, thumbnailSize), this); + m_brushShortcutsFilterModel = new BrushShortcutFilterProxyModel(tagModel); + connect( + m_filter, &ShortcutFilterInput::conflictBoxChecked, + m_brushShortcutsFilterModel, + &BrushShortcutFilterProxyModel::setSearchAllTags); + + QWidget *widget = new QWidget; + QVBoxLayout *layout = new QVBoxLayout(widget); + + QComboBox *tagCombo = new QComboBox; + tagCombo->setEditable(false); + tagCombo->setModel(tagModel); + layout->addWidget(tagCombo); + connect( + tagCombo, QOverload::of(&QComboBox::currentIndexChanged), + m_brushShortcutsFilterModel, + &BrushShortcutFilterProxyModel::setCurrentTagRow); + connect( + m_filter, &ShortcutFilterInput::conflictBoxChecked, tagCombo, + &QComboBox::setDisabled); + + m_brushesTable = ProportionalTableView::make( + m_filter, int(BrushShortcutModel::FilterRole), m_brushShortcutsModel, + m_brushShortcutsFilterModel, false); + connect( + m_filter, &ShortcutFilterInput::filtered, m_brushShortcutsFilterModel, + &BrushShortcutFilterProxyModel::setSearchString); + m_brushesTable->setColumnStretches({6, 2}); + m_brushesTable->verticalHeader()->setSectionResizeMode( + QHeaderView::ResizeToContents); + utils::bindKineticScrolling(m_brushesTable); + + m_brushesTable->setItemDelegateForColumn(1, keySequenceDelegate); + + layout->addWidget(m_brushesTable, 1); + + return widget; } static QModelIndex @@ -182,49 +285,50 @@ static void execCanvasShortcutDialog( } } -QWidget *Shortcuts::initCanvasShortcuts( - desktop::settings::Settings &settings, QLineEdit *filter) +QWidget *Shortcuts::initCanvasShortcuts(desktop::settings::Settings &settings) { QWidget *widget = new QWidget; QVBoxLayout *layout = new QVBoxLayout(widget); - CanvasShortcutsModel *shortcutsModel = new CanvasShortcutsModel(this); - shortcutsModel->loadShortcuts(settings.canvasShortcuts()); - onAnyModelChange(shortcutsModel, &settings, [=, &settings] { - settings.setCanvasShortcuts(shortcutsModel->saveShortcuts()); - }); - - ProportionalTableView *shortcuts = - ProportionalTableView::make(filter, shortcutsModel); - shortcuts->setColumnStretches({6, 3, 3, 0}); - utils::bindKineticScrolling(shortcuts); + m_canvasShortcutsModel = new CanvasShortcutsModel(this); + m_canvasShortcutsModel->loadShortcuts(settings.canvasShortcuts()); + std::function updateModelFn = [=, &settings] { + settings.setCanvasShortcuts(m_canvasShortcutsModel->saveShortcuts()); + }; + ON_ANY_MODEL_CHANGE(m_canvasShortcutsModel, &settings, updateModelFn); + + m_canvasTable = ProportionalTableView::make( + m_filter, int(CanvasShortcutsModel::FilterRole), + m_canvasShortcutsModel); + m_canvasTable->setColumnStretches({6, 3, 3, 0}); + utils::bindKineticScrolling(m_canvasTable); connect( - shortcuts, &QAbstractItemView::activated, this, + m_canvasTable, &QAbstractItemView::activated, this, [=](const QModelIndex &index) { - if(const auto *shortcut = shortcutsModel->shortcutAt( - mapFromView(shortcuts, index).row())) { + if(const auto *shortcut = m_canvasShortcutsModel->shortcutAt( + mapFromView(m_canvasTable, index).row())) { execCanvasShortcutDialog( - shortcuts, shortcut, shortcutsModel, this, + m_canvasTable, shortcut, m_canvasShortcutsModel, this, tr("Edit Canvas Shortcut"), [=](auto newShortcut) { - return shortcutsModel->editShortcut( + return m_canvasShortcutsModel->editShortcut( *shortcut, newShortcut); }); } }); - layout->addWidget(shortcuts, 1); + layout->addWidget(m_canvasTable, 1); utils::EncapsulatedLayout *actions = listActions( - shortcuts, tr("Add canvas shortcut…"), + m_canvasTable, tr("Add canvas shortcut…"), [=] { execCanvasShortcutDialog( - shortcuts, nullptr, shortcutsModel, this, + m_canvasTable, nullptr, m_canvasShortcutsModel, this, tr("New Canvas Shortcut"), [=](auto newShortcut) { - return shortcutsModel->addShortcut(newShortcut); + return m_canvasShortcutsModel->addShortcut(newShortcut); }); }, tr("Remove selected canvas shortcut…"), makeDefaultDeleter( - this, shortcuts, tr("Remove canvas shortcut"), + this, m_canvasTable, tr("Remove canvas shortcut"), QT_TR_N_NOOP("Really remove %n canvas shortcut(s)?"))); actions->addStretch(); @@ -236,7 +340,7 @@ QWidget *Shortcuts::initCanvasShortcuts( tr("Really restore all canvas shortcuts to their default values?"), this); if(confirm) { - shortcutsModel->restoreDefaults(); + m_canvasShortcutsModel->restoreDefaults(); } }); actions->addWidget(restoreDefaults); @@ -245,130 +349,108 @@ QWidget *Shortcuts::initCanvasShortcuts( return widget; } -QWidget *Shortcuts::initTouchShortcuts(desktop::settings::Settings &settings) +void Shortcuts::updateConflicts() { - QScrollArea *scroll = new QScrollArea; - scroll->setFrameStyle(QScrollArea::NoFrame); - scroll->setWidgetResizable(true); - utils::bindKineticScrolling(scroll); + if(!m_updatingConflicts) { + QScopedValueRollback rollback(m_updatingConflicts, true); + + QSet actionKeySequences; + int actionShortcutCount = m_actionShortcutsModel->rowCount(); + for(int i = 0; i < actionShortcutCount; ++i) { + const CustomShortcut &cs = m_actionShortcutsModel->shortcutAt(i); + for(const QKeySequence &shortcut : + {cs.currentShortcut, cs.alternateShortcut}) { + if(!shortcut.isEmpty()) { + actionKeySequences.insert(shortcut); + } + } + } - QWidget *widget = new QWidget; - QVBoxLayout *layout = new QVBoxLayout(widget); - scroll->setWidget(widget); - - QFormLayout *modeForm = utils::addFormSection(layout); - QButtonGroup *touchMode = utils::addRadioGroup( - modeForm, tr("Touch mode:"), true, - { - {tr("Touchscreen"), false}, - {tr("Gestures"), true}, - }); - settings.bindTouchGestures(touchMode); - - utils::addFormSeparator(layout); - QFormLayout *touchForm = utils::addFormSection(layout); - - QComboBox *oneFingerTouch = new QComboBox; - oneFingerTouch->setSizeAdjustPolicy(QComboBox::AdjustToContents); - oneFingerTouch->addItem( - tr("No action"), int(desktop::settings::OneFingerTouchAction::Nothing)); - oneFingerTouch->addItem( - tr("Draw"), int(desktop::settings::OneFingerTouchAction::Draw)); - oneFingerTouch->addItem( - tr("Pan canvas"), int(desktop::settings::OneFingerTouchAction::Pan)); - oneFingerTouch->addItem( - tr("Guess"), int(desktop::settings::OneFingerTouchAction::Guess)); - settings.bindOneFingerTouch(oneFingerTouch, Qt::UserRole); - touchForm->addRow(tr("One-finger touch:"), oneFingerTouch); - - QComboBox *twoFingerPinch = new QComboBox; - twoFingerPinch->setSizeAdjustPolicy(QComboBox::AdjustToContents); - twoFingerPinch->addItem( - tr("No action"), int(desktop::settings::TwoFingerPinchAction::Nothing)); - twoFingerPinch->addItem( - tr("Zoom"), int(desktop::settings::TwoFingerPinchAction::Zoom)); - settings.bindTwoFingerPinch(twoFingerPinch, Qt::UserRole); - touchForm->addRow(tr("Two-finger pinch:"), twoFingerPinch); - - QComboBox *twoFingerTwist = new QComboBox; - twoFingerTwist->setSizeAdjustPolicy(QComboBox::AdjustToContents); - twoFingerTwist->addItem( - tr("No action"), int(desktop::settings::TwoFingerPinchAction::Nothing)); - twoFingerTwist->addItem( - tr("Rotate canvas"), - int(desktop::settings::TwoFingerTwistAction::Rotate)); - twoFingerTwist->addItem( - tr("Free rotate canvas"), - int(desktop::settings::TwoFingerTwistAction::RotateNoSnap)); - twoFingerTwist->addItem( - tr("Ratchet rotate canvas"), - int(desktop::settings::TwoFingerTwistAction::RotateDiscrete)); - settings.bindTwoFingerTwist(twoFingerTwist, Qt::UserRole); - touchForm->addRow(tr("Two-finger twist:"), twoFingerTwist); - - utils::addFormSeparator(layout); - QFormLayout *tapForm = utils::addFormSection(layout); - - QComboBox *oneFingerTap = new QComboBox; - QComboBox *twoFingerTap = new QComboBox; - QComboBox *threeFingerTap = new QComboBox; - QComboBox *fourFingerTap = new QComboBox; - for(QComboBox *tap : - {oneFingerTap, twoFingerTap, threeFingerTap, fourFingerTap}) { - tap->addItem( - tr("No action"), int(desktop::settings::TouchTapAction::Nothing)); - tap->addItem(tr("Undo"), int(desktop::settings::TouchTapAction::Undo)); - tap->addItem(tr("Redo"), int(desktop::settings::TouchTapAction::Redo)); - tap->addItem( - tr("Hide docks"), - int(desktop::settings::TouchTapAction::HideDocks)); - tap->addItem( - tr("Toggle color picker"), - int(desktop::settings::TouchTapAction::ColorPicker)); - tap->addItem( - tr("Toggle eraser"), - int(desktop::settings::TouchTapAction::Eraser)); - tap->addItem( - tr("Toggle erase mode"), - int(desktop::settings::TouchTapAction::EraseMode)); - tap->addItem( - tr("Toggle recolor mode"), - int(desktop::settings::TouchTapAction::RecolorMode)); + QSet brushKeySequences; + int brushShortcutCount = m_brushShortcutsModel->rowCount(); + for(int i = 0; i < brushShortcutCount; ++i) { + brushKeySequences.insert(m_brushShortcutsModel->shortcutAt(i)); + } + + QSet canvasKeySequences; + int canvasShortcutCount = m_canvasShortcutsModel->rowCount(); + for(int i = 0; i < canvasShortcutCount; ++i) { + const CanvasShortcuts::Shortcut *s = + m_canvasShortcutsModel->shortcutAt(i); + if(s) { + canvasKeySequences += s->keySequences(); + } + } + + m_actionShortcutsModel->setExternalKeySequences( + brushKeySequences + canvasKeySequences); + m_brushShortcutsModel->setExternalKeySequences( + actionKeySequences + canvasKeySequences); + m_canvasShortcutsModel->setExternalKeySequences( + actionKeySequences + brushKeySequences); + + QTabBar *bar = m_tabs->tabBar(); + QColor conflictColor = m_tabs->palette().buttonText().color(); + conflictColor.setRedF(conflictColor.redF() * 0.5 + 0.5); + conflictColor.setGreenF(conflictColor.greenF() * 0.5); + conflictColor.setBlueF(conflictColor.blueF() * 0.5); + bar->setTabTextColor( + ACTION_TAB, + m_actionShortcutsModel->hasConflicts() ? conflictColor : QColor()); + bar->setTabTextColor( + BRUSH_TAB, + m_brushShortcutsModel->hasConflicts() ? conflictColor : QColor()); + bar->setTabTextColor( + CANVAS_TAB, + m_canvasShortcutsModel->hasConflicts() ? conflictColor : QColor()); + + updateTabTexts(); } +} - settings.bindOneFingerTap(oneFingerTap, Qt::UserRole); - settings.bindTwoFingerTap(twoFingerTap, Qt::UserRole); - settings.bindThreeFingerTap(threeFingerTap, Qt::UserRole); - settings.bindFourFingerTap(fourFingerTap, Qt::UserRole); - - settings.bindTouchGestures(twoFingerTap, &QComboBox::setDisabled); - settings.bindTouchGestures(threeFingerTap, &QComboBox::setDisabled); - settings.bindTouchGestures(fourFingerTap, &QComboBox::setDisabled); - - tapForm->addRow(tr("One-finger tap:"), oneFingerTap); - tapForm->addRow(tr("Two-finger tap:"), twoFingerTap); - tapForm->addRow(tr("Three-finger tap:"), threeFingerTap); - tapForm->addRow(tr("Four-finger tap:"), fourFingerTap); - - utils::addFormSeparator(layout); - QFormLayout *tapAndHoldForm = utils::addFormSection(layout); - - QComboBox *oneFingerTapAndHold = new QComboBox; - for(QComboBox *tapAndHold : {oneFingerTapAndHold}) { - tapAndHold->addItem( - tr("No action"), - int(desktop::settings::TouchTapAndHoldAction::Nothing)); - tapAndHold->addItem( - tr("Pick color"), - int(desktop::settings::TouchTapAndHoldAction::ColorPickMode)); +void Shortcuts::updateTabTexts() +{ + if(m_filter->isEmpty()) { + m_tabs->setTabText(ACTION_TAB, actionTabText()); + m_tabs->setTabText(BRUSH_TAB, brushTabText()); + m_tabs->setTabText(CANVAS_TAB, canvasTabText()); + } else { + m_tabs->setTabText( + ACTION_TAB, + searchResultText( + actionTabText(), m_actionsTable->model()->rowCount())); + m_tabs->setTabText( + BRUSH_TAB, + searchResultText( + brushTabText(), m_brushesTable->model()->rowCount())); + m_tabs->setTabText( + CANVAS_TAB, + searchResultText( + canvasTabText(), m_canvasTable->model()->rowCount())); } +} - settings.bindOneFingerTapAndHold(oneFingerTapAndHold, Qt::UserRole); - settings.bindTouchGestures(oneFingerTapAndHold, &QComboBox::setDisabled); - tapAndHoldForm->addRow(tr("One-finger tap and hold:"), oneFingerTapAndHold); +QString Shortcuts::actionTabText() +{ + return tr("Actions"); +} - layout->addStretch(); - return scroll; +QString Shortcuts::brushTabText() +{ + return tr("Brushes"); +} + +QString Shortcuts::canvasTabText() +{ + return tr("Canvas"); +} + +QString Shortcuts::searchResultText(const QString &text, int results) +{ + //: This is for showing search feedback in the tabs of the shortcut + //: preferences. %1 is the original tab title, like "Actions" or "Brushes", + //: %2 is the number of search results in that tab. + return tr("%1 (%2)").arg(text).arg(results); } } // namespace settingsdialog diff --git a/src/desktop/dialogs/settingsdialog/shortcuts.h b/src/desktop/dialogs/settingsdialog/shortcuts.h index 509dabc58..e9cfd1650 100644 --- a/src/desktop/dialogs/settingsdialog/shortcuts.h +++ b/src/desktop/dialogs/settingsdialog/shortcuts.h @@ -1,11 +1,15 @@ // SPDX-License-Identifier: GPL-3.0-or-later #ifndef DESKTOP_DIALOGS_SETTINGSDIALOG_SHORTCUTS_H #define DESKTOP_DIALOGS_SETTINGSDIALOG_SHORTCUTS_H +#include "libclient/utils/debouncetimer.h" #include #include +class BrushShortcutModel; +class BrushShortcutFilterProxyModel; +class CanvasShortcutsModel; class CustomShortcutModel; -class QLineEdit; +class QStyledItemDelegate; class QTabWidget; class QVBoxLayout; @@ -19,28 +23,52 @@ namespace dialogs { namespace settingsdialog { class ProportionalTableView; +class ShortcutFilterInput; class Shortcuts final : public QWidget { Q_OBJECT public: Shortcuts(desktop::settings::Settings &settings, QWidget *parent = nullptr); + void initiateFixShortcutConflicts(); + void initiateBrushShortcutChange(int presetId); + public slots: void finishEditing(); private: + static constexpr int ACTION_TAB = 0; + static constexpr int BRUSH_TAB = 1; + static constexpr int CANVAS_TAB = 2; + QWidget *initActionShortcuts( - desktop::settings::Settings &settings, QLineEdit *filter); + desktop::settings::Settings &settings, + QStyledItemDelegate *keySequenceDelegate); + + QWidget *initBrushShortcuts(QStyledItemDelegate *keySequenceDelegate); + + QWidget *initCanvasShortcuts(desktop::settings::Settings &settings); - QWidget *initCanvasShortcuts( - desktop::settings::Settings &settings, QLineEdit *filter); + void updateConflicts(); + void updateTabTexts(); - QWidget *initTouchShortcuts(desktop::settings::Settings &settings); + static QString actionTabText(); + static QString brushTabText(); + static QString canvasTabText(); + static QString searchResultText(const QString &text, int results); - QTabWidget *m_tabs; - CustomShortcutModel *m_globalShortcutsModel; + ShortcutFilterInput *m_filter = nullptr; + QTabWidget *m_tabs = nullptr; + CustomShortcutModel *m_actionShortcutsModel = nullptr; + CanvasShortcutsModel *m_canvasShortcutsModel = nullptr; + BrushShortcutModel *m_brushShortcutsModel = nullptr; + BrushShortcutFilterProxyModel *m_brushShortcutsFilterModel = nullptr; + ProportionalTableView *m_actionsTable = nullptr; + ProportionalTableView *m_brushesTable = nullptr; + ProportionalTableView *m_canvasTable = nullptr; QItemEditorFactory m_itemEditorFactory; - ProportionalTableView *m_shortcutsTable; + DebounceTimer m_shortcutConflictDebounce; + bool m_updatingConflicts = false; }; } // namespace settingsdialog diff --git a/src/desktop/dialogs/settingsdialog/input.cpp b/src/desktop/dialogs/settingsdialog/tablet.cpp similarity index 60% rename from src/desktop/dialogs/settingsdialog/input.cpp rename to src/desktop/dialogs/settingsdialog/tablet.cpp index d423c740f..ad7916acb 100644 --- a/src/desktop/dialogs/settingsdialog/input.cpp +++ b/src/desktop/dialogs/settingsdialog/tablet.cpp @@ -1,15 +1,16 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#include "desktop/dialogs/settingsdialog/input.h" +#include "desktop/dialogs/settingsdialog/tablet.h" #include "desktop/settings.h" #include "desktop/utils/widgetutils.h" #include "desktop/widgets/curvewidget.h" #include "desktop/widgets/kis_slider_spin_box.h" -#include "desktop/widgets/tablettest.h" #include +#include #include #include #include #include +#include #include #include #include @@ -17,38 +18,43 @@ namespace dialogs { namespace settingsdialog { -Input::Input(desktop::settings::Settings &settings, QWidget *parent) +Tablet::Tablet(desktop::settings::Settings &settings, QWidget *parent) : Page(parent) { init(settings); } -void Input::setUp(desktop::settings::Settings &settings, QVBoxLayout *layout) +void Tablet::createButtons(QDialogButtonBox *buttons) { - initTablet(settings, layout); - utils::addFormSeparator(layout); - initPressureCurve(settings, utils::addFormSection(layout)); -#ifdef Q_OS_ANDROID - utils::addFormSeparator(layout); - initAndroid(settings, utils::addFormSection(layout)); -#endif + m_tabletTesterButton = + new QPushButton(QIcon::fromTheme("input-tablet"), tr("Tablet Tester")); + m_tabletTesterButton->setAutoDefault(false); + buttons->addButton(m_tabletTesterButton, QDialogButtonBox::ActionRole); + m_tabletTesterButton->setEnabled(false); + m_tabletTesterButton->setVisible(false); + connect( + m_tabletTesterButton, &QPushButton::clicked, this, + &Tablet::tabletTesterRequested); } -#ifdef Q_OS_ANDROID -void Input::initAndroid( - desktop::settings::Settings &settings, QFormLayout *form) +void Tablet::showButtons() { - auto *captureVolumeRocker = new QCheckBox(tr("Capture volume rocker")); - settings.bindCaptureVolumeRocker(captureVolumeRocker); - form->addRow(tr("Android:"), captureVolumeRocker); + m_tabletTesterButton->setEnabled(true); + m_tabletTesterButton->setVisible(true); } -#endif -void Input::initPressureCurve( +void Tablet::setUp(desktop::settings::Settings &settings, QVBoxLayout *layout) +{ + initTablet(settings, utils::addFormSection(layout)); + utils::addFormSeparator(layout); + initPressureCurve(settings, utils::addFormSection(layout)); +} + +void Tablet::initPressureCurve( desktop::settings::Settings &settings, QFormLayout *form) { auto *curve = new widgets::CurveWidget(this); - curve->setAxisTitleLabels(tr("Input"), tr("Output")); + curve->setAxisTitleLabels(tr("Tablet"), tr("Output")); curve->setCurveSize(200, 200); settings.bindGlobalPressureCurve( curve, &widgets::CurveWidget::setCurveFromString); @@ -60,35 +66,58 @@ void Input::initPressureCurve( form->addRow(tr("Global pressure curve:"), curve); } -void Input::initTablet( - desktop::settings::Settings &settings, QVBoxLayout *layout) +void Tablet::initTablet( + desktop::settings::Settings &settings, QFormLayout *form) { - QHBoxLayout *section = new QHBoxLayout; - layout->addLayout(section); +#if defined(Q_OS_WIN) + QComboBox *driver = new QComboBox; + driver->addItem( + tr("KisTablet Windows Ink"), + QVariant::fromValue(tabletinput::Mode::KisTabletWinink)); + driver->addItem( + tr("KisTablet Wintab"), + QVariant::fromValue(tabletinput::Mode::KisTabletWintab)); + driver->addItem( + tr("KisTablet Wintab Relative"), + QVariant::fromValue(tabletinput::Mode::KisTabletWintabRelativePenHack)); +# if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + driver->addItem(tr("Qt5"), QVariant::fromValue(tabletinput::Mode::Qt5)); +# else + driver->addItem( + tr("Qt6 Windows Ink"), + QVariant::fromValue(tabletinput::Mode::Qt6Winink)); + driver->addItem( + tr("Qt6 Wintab"), QVariant::fromValue(tabletinput::Mode::Qt6Wintab)); +# endif - QFormLayout *form = utils::addFormSection(section); + settings.bindTabletDriver(driver, Qt::UserRole); +#else + QLabel *driver = + new QLabel(QStringLiteral("Qt %1").arg(QString::fromUtf8(qVersion()))); +#endif + form->addRow(tr("Tablet driver:"), driver); auto *pressure = new QCheckBox(tr("Enable pressure sensitivity")); settings.bindTabletEvents(pressure); - form->addRow(tr("Tablet:"), pressure); - - auto *interpolate = new QCheckBox(tr("Compensate jagged curves")); - settings.bindInterpolateInputs(interpolate); - form->addRow(nullptr, interpolate); + form->addRow(tr("Pen pressure:"), pressure); auto *smoothing = new KisSliderSpinBox; smoothing->setMaximum(libclient::settings::maxSmoothing); smoothing->setPrefix(tr("Global smoothing: ")); settings.bindSmoothing(smoothing); - form->addRow(nullptr, smoothing); + form->addRow(tr("Smoothing:"), smoothing); auto *mouseSmoothing = new QCheckBox(tr("Apply global smoothing to mouse")); settings.bindMouseSmoothing(mouseSmoothing); form->addRow(nullptr, mouseSmoothing); + auto *interpolate = new QCheckBox(tr("Compensate jagged curves")); + settings.bindInterpolateInputs(interpolate); + form->addRow(nullptr, interpolate); + auto *eraserAction = new QComboBox; eraserAction->addItem( - tr("Do nothing"), int(tabletinput::EraserAction::Ignore)); + tr("Treat as regular pen tip"), int(tabletinput::EraserAction::Ignore)); #ifndef __EMSCRIPTEN__ eraserAction->addItem( tr("Switch to eraser slot"), int(tabletinput::EraserAction::Switch)); @@ -97,49 +126,8 @@ void Input::initTablet( tr("Erase with current brush"), int(tabletinput::EraserAction::Override)); settings.bindTabletEraserAction(eraserAction, Qt::UserRole); - //: Eraser refers to the eraser tip of a tablet pen. - form->addRow(tr("Eraser action:"), eraserAction); - -#ifdef Q_OS_WIN - utils::addFormSpacer(form); - - auto *driver = new QComboBox; - driver->addItem( - tr("KisTablet Windows Ink"), - QVariant::fromValue(tabletinput::Mode::KisTabletWinink)); - driver->addItem( - tr("KisTablet Wintab"), - QVariant::fromValue(tabletinput::Mode::KisTabletWintab)); - driver->addItem( - tr("KisTablet Wintab Relative"), - QVariant::fromValue(tabletinput::Mode::KisTabletWintabRelativePenHack)); -# if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - driver->addItem(tr("Qt5"), QVariant::fromValue(tabletinput::Mode::Qt5)); -# else - driver->addItem( - tr("Qt6 Windows Ink"), - QVariant::fromValue(tabletinput::Mode::Qt6Winink)); - driver->addItem( - tr("Qt6 Wintab"), QVariant::fromValue(tabletinput::Mode::Qt6Wintab)); -# endif - - settings.bindTabletDriver(driver, Qt::UserRole); - form->addRow(tr("Driver:"), driver); -#endif - - auto *testerLayout = new QVBoxLayout; - auto *testerLabel = new QLabel(tr("Test your tablet here:")); - testerLabel->setAlignment(Qt::AlignHCenter); - testerLayout->addWidget(testerLabel); - auto *testerFrame = new QFrame; - testerFrame->setFrameShape(QFrame::StyledPanel); - testerFrame->setFrameShadow(QFrame::Sunken); - testerFrame->setFixedSize(194, 194); - auto *testerFrameLayout = new QHBoxLayout(testerFrame); - testerFrameLayout->setContentsMargins(0, 0, 0, 0); - testerFrameLayout->addWidget(new widgets::TabletTester); - testerLayout->addWidget(testerFrame); - section->addLayout(testerLayout); + //: This refers to the eraser end tablet pen, not a tooltip or something. + form->addRow(tr("Eraser tip behavior:"), eraserAction); } } // namespace settingsdialog diff --git a/src/desktop/dialogs/settingsdialog/input.h b/src/desktop/dialogs/settingsdialog/tablet.h similarity index 50% rename from src/desktop/dialogs/settingsdialog/input.h rename to src/desktop/dialogs/settingsdialog/tablet.h index f678e0e3c..d74d04b69 100644 --- a/src/desktop/dialogs/settingsdialog/input.h +++ b/src/desktop/dialogs/settingsdialog/tablet.h @@ -1,8 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#ifndef DESKTOP_DIALOGS_SETTINGSDIALOG_INPUT_H -#define DESKTOP_DIALOGS_SETTINGSDIALOG_INPUT_H +#ifndef DESKTOP_DIALOGS_SETTINGSDIALOG_TABLET_H +#define DESKTOP_DIALOGS_SETTINGSDIALOG_TABLET_H #include "desktop/dialogs/settingsdialog/page.h" +class QDialogButtonBox; +class QPushButton; class QFormLayout; namespace desktop { @@ -14,24 +16,28 @@ class Settings; namespace dialogs { namespace settingsdialog { -class Input final : public Page { +class Tablet final : public Page { Q_OBJECT public: - Input(desktop::settings::Settings &settings, QWidget *parent = nullptr); + Tablet(desktop::settings::Settings &settings, QWidget *parent = nullptr); + + void createButtons(QDialogButtonBox *buttons); + void showButtons(); + +signals: + void tabletTesterRequested(); protected: void setUp(desktop::settings::Settings &settings, QVBoxLayout *layout) override; private: -#ifdef Q_OS_ANDROID - void initAndroid(desktop::settings::Settings &settings, QFormLayout *form); -#endif - void initPressureCurve(desktop::settings::Settings &settings, QFormLayout *form); - void initTablet(desktop::settings::Settings &settings, QVBoxLayout *layout); + void initTablet(desktop::settings::Settings &settings, QFormLayout *form); + + QPushButton *m_tabletTesterButton = nullptr; }; } // namespace settingsdialog diff --git a/src/desktop/dialogs/settingsdialog/touch.cpp b/src/desktop/dialogs/settingsdialog/touch.cpp new file mode 100644 index 000000000..47d08027a --- /dev/null +++ b/src/desktop/dialogs/settingsdialog/touch.cpp @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "desktop/dialogs/settingsdialog/touch.h" +#include "desktop/settings.h" +#include "desktop/utils/widgetutils.h" +#include +#include +#include +#include +#include +#include +#include + +namespace dialogs { +namespace settingsdialog { + +Touch::Touch(desktop::settings::Settings &settings, QWidget *parent) + : Page(parent) +{ + init(settings); +} + +void Touch::createButtons(QDialogButtonBox *buttons) +{ + m_touchTesterButton = new QPushButton( + QIcon::fromTheme("input-touchscreen"), tr("Touch Tester")); + m_touchTesterButton->setAutoDefault(false); + buttons->addButton(m_touchTesterButton, QDialogButtonBox::ActionRole); + m_touchTesterButton->setEnabled(false); + m_touchTesterButton->setVisible(false); + connect( + m_touchTesterButton, &QPushButton::clicked, this, + &Touch::touchTesterRequested); +} + +void Touch::showButtons() +{ + m_touchTesterButton->setEnabled(true); + m_touchTesterButton->setVisible(true); +} + +void Touch::setUp(desktop::settings::Settings &settings, QVBoxLayout *layout) +{ + initMode(settings, utils::addFormSection(layout)); + utils::addFormSeparator(layout); + initTouchActions(settings, utils::addFormSection(layout)); + utils::addFormSeparator(layout); + initTapActions(settings, utils::addFormSection(layout)); + utils::addFormSeparator(layout); + initTapAndHoldActions(settings, utils::addFormSection(layout)); +} + +void Touch::initMode(desktop::settings::Settings &settings, QFormLayout *form) +{ + QButtonGroup *touchMode = utils::addRadioGroup( + form, tr("Touch mode:"), true, + { + {tr("Touchscreen"), false}, + {tr("Gestures"), true}, + }); + settings.bindTouchGestures(touchMode); +} + +void Touch::initTapActions( + desktop::settings::Settings &settings, QFormLayout *form) +{ + QComboBox *oneFingerTap = new QComboBox; + QComboBox *twoFingerTap = new QComboBox; + QComboBox *threeFingerTap = new QComboBox; + QComboBox *fourFingerTap = new QComboBox; + for(QComboBox *tap : + {oneFingerTap, twoFingerTap, threeFingerTap, fourFingerTap}) { + tap->addItem( + tr("No action"), int(desktop::settings::TouchTapAction::Nothing)); + tap->addItem(tr("Undo"), int(desktop::settings::TouchTapAction::Undo)); + tap->addItem(tr("Redo"), int(desktop::settings::TouchTapAction::Redo)); + tap->addItem( + tr("Hide docks"), + int(desktop::settings::TouchTapAction::HideDocks)); + tap->addItem( + tr("Toggle color picker"), + int(desktop::settings::TouchTapAction::ColorPicker)); + tap->addItem( + tr("Toggle eraser"), + int(desktop::settings::TouchTapAction::Eraser)); + tap->addItem( + tr("Toggle erase mode"), + int(desktop::settings::TouchTapAction::EraseMode)); + tap->addItem( + tr("Toggle recolor mode"), + int(desktop::settings::TouchTapAction::RecolorMode)); + } + + settings.bindOneFingerTap(oneFingerTap, Qt::UserRole); + settings.bindTwoFingerTap(twoFingerTap, Qt::UserRole); + settings.bindThreeFingerTap(threeFingerTap, Qt::UserRole); + settings.bindFourFingerTap(fourFingerTap, Qt::UserRole); + + settings.bindTouchGestures(twoFingerTap, &QComboBox::setDisabled); + settings.bindTouchGestures(threeFingerTap, &QComboBox::setDisabled); + settings.bindTouchGestures(fourFingerTap, &QComboBox::setDisabled); + + form->addRow(tr("One-finger tap:"), oneFingerTap); + form->addRow(tr("Two-finger tap:"), twoFingerTap); + form->addRow(tr("Three-finger tap:"), threeFingerTap); + form->addRow(tr("Four-finger tap:"), fourFingerTap); +} + +void Touch::initTapAndHoldActions( + desktop::settings::Settings &settings, QFormLayout *form) +{ + QComboBox *oneFingerTapAndHold = new QComboBox; + for(QComboBox *tapAndHold : {oneFingerTapAndHold}) { + tapAndHold->addItem( + tr("No action"), + int(desktop::settings::TouchTapAndHoldAction::Nothing)); + tapAndHold->addItem( + tr("Pick color"), + int(desktop::settings::TouchTapAndHoldAction::ColorPickMode)); + } + + settings.bindOneFingerTapAndHold(oneFingerTapAndHold, Qt::UserRole); + + settings.bindTouchGestures(oneFingerTapAndHold, &QComboBox::setDisabled); + + form->addRow(tr("One-finger tap and hold:"), oneFingerTapAndHold); +} + +void Touch::initTouchActions( + desktop::settings::Settings &settings, QFormLayout *form) +{ + QComboBox *oneFingerTouch = new QComboBox; + oneFingerTouch->setSizeAdjustPolicy(QComboBox::AdjustToContents); + oneFingerTouch->addItem( + tr("No action"), int(desktop::settings::OneFingerTouchAction::Nothing)); + oneFingerTouch->addItem( + tr("Draw"), int(desktop::settings::OneFingerTouchAction::Draw)); + oneFingerTouch->addItem( + tr("Pan canvas"), int(desktop::settings::OneFingerTouchAction::Pan)); + oneFingerTouch->addItem( + tr("Guess"), int(desktop::settings::OneFingerTouchAction::Guess)); + settings.bindOneFingerTouch(oneFingerTouch, Qt::UserRole); + form->addRow(tr("One-finger touch:"), oneFingerTouch); + + QComboBox *twoFingerPinch = new QComboBox; + twoFingerPinch->setSizeAdjustPolicy(QComboBox::AdjustToContents); + twoFingerPinch->addItem( + tr("No action"), int(desktop::settings::TwoFingerPinchAction::Nothing)); + twoFingerPinch->addItem( + tr("Zoom"), int(desktop::settings::TwoFingerPinchAction::Zoom)); + settings.bindTwoFingerPinch(twoFingerPinch, Qt::UserRole); + form->addRow(tr("Two-finger pinch:"), twoFingerPinch); + + QComboBox *twoFingerTwist = new QComboBox; + twoFingerTwist->setSizeAdjustPolicy(QComboBox::AdjustToContents); + twoFingerTwist->addItem( + tr("No action"), int(desktop::settings::TwoFingerPinchAction::Nothing)); + twoFingerTwist->addItem( + tr("Rotate canvas"), + int(desktop::settings::TwoFingerTwistAction::Rotate)); + twoFingerTwist->addItem( + tr("Free rotate canvas"), + int(desktop::settings::TwoFingerTwistAction::RotateNoSnap)); + twoFingerTwist->addItem( + tr("Ratchet rotate canvas"), + int(desktop::settings::TwoFingerTwistAction::RotateDiscrete)); + settings.bindTwoFingerTwist(twoFingerTwist, Qt::UserRole); + form->addRow(tr("Two-finger twist:"), twoFingerTwist); +} + +} // namespace settingsdialog +} // namespace dialogs diff --git a/src/desktop/dialogs/settingsdialog/touch.h b/src/desktop/dialogs/settingsdialog/touch.h new file mode 100644 index 000000000..c848c7a3d --- /dev/null +++ b/src/desktop/dialogs/settingsdialog/touch.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#ifndef DESKTOP_DIALOGS_SETTINGSDIALOG_TOUCH_H +#define DESKTOP_DIALOGS_SETTINGSDIALOG_TOUCH_H +#include "desktop/dialogs/settingsdialog/page.h" + +class QDialogButtonBox; +class QFormLayout; +class QPushButton; + +namespace desktop { +namespace settings { +class Settings; +} +} + +namespace dialogs { +namespace settingsdialog { + +class Touch final : public Page { + Q_OBJECT +public: + Touch(desktop::settings::Settings &settings, QWidget *parent = nullptr); + + void createButtons(QDialogButtonBox *buttons); + void showButtons(); + +signals: + void touchTesterRequested(); + +protected: + void + setUp(desktop::settings::Settings &settings, QVBoxLayout *layout) override; + +private: + void initMode(desktop::settings::Settings &settings, QFormLayout *form); + + void + initTapActions(desktop::settings::Settings &settings, QFormLayout *form); + + void initTapAndHoldActions( + desktop::settings::Settings &settings, QFormLayout *form); + + void + initTouchActions(desktop::settings::Settings &settings, QFormLayout *form); + + QPushButton *m_touchTesterButton; +}; + +} +} + +#endif diff --git a/src/desktop/dialogs/startdialog/links.cpp b/src/desktop/dialogs/startdialog/links.cpp index 1e314d6b6..e7fcdf42c 100644 --- a/src/desktop/dialogs/startdialog/links.cpp +++ b/src/desktop/dialogs/startdialog/links.cpp @@ -28,7 +28,7 @@ Links::Links(bool vertical, QWidget *parent) {"help-contents", tr("Help"), tr("Open Drawpile's help pages in your browser"), QUrl{"https://drawpile.net/help/"}}, - {"dialog-input-devices", tr("Tablet Setup"), + {"input-tablet", tr("Tablet Setup"), tr("Open Drawpile's tablet setup and troubleshooting help page"), QUrl{"https://docs.drawpile.net/help/tech/tablet"}}, {"user-group-new", tr("Communities"), diff --git a/src/desktop/docks/brushpalettedock.cpp b/src/desktop/docks/brushpalettedock.cpp index 6cb494e22..0b5b74ff7 100644 --- a/src/desktop/docks/brushpalettedock.cpp +++ b/src/desktop/docks/brushpalettedock.cpp @@ -405,6 +405,26 @@ void BrushPalette::overwriteCurrentPreset(QWidget *parent) box->show(); } +void BrushPalette::setSelectedPresetIdsFromShortcut( + const QKeySequence &shortcut) +{ + QVector ids = d->presetModel->getPresetIdsForShortcut(shortcut); + int count = ids.size(); + if(count != 0) { + int currentIndex = ids.indexOf(d->selectedPresetId); + int i = currentIndex < 0 ? 0 : ((currentIndex + 1) % count); + setSelectedPresetId(ids[i]); + if(d->brushSettings && + d->brushSettings->currentPresetId() != d->selectedPresetId) { + std::optional opt = + d->presetModel->searchPresetBrushData(d->selectedPresetId); + if(opt.has_value()) { + d->brushSettings->setCurrentBrushPreset(opt.value()); + } + } + } +} + void BrushPalette::resetAllPresets() { d->presetModel->resetAllPresetChanges(); diff --git a/src/desktop/docks/brushpalettedock.h b/src/desktop/docks/brushpalettedock.h index a52d7f7a1..a8b5a5941 100644 --- a/src/desktop/docks/brushpalettedock.h +++ b/src/desktop/docks/brushpalettedock.h @@ -3,6 +3,7 @@ #define DESKTOP_DOCKS_BRUSHPALETTEDOCK_H #include "desktop/docks/dockbase.h" +class QKeySequence; class QTemporaryFile; namespace brushes { @@ -36,6 +37,7 @@ class BrushPalette final : public DockBase { void newPreset(); void overwriteCurrentPreset(QWidget *parent); + void setSelectedPresetIdsFromShortcut(const QKeySequence &shortcut); public slots: void resetAllPresets(); diff --git a/src/desktop/mainwindow.cpp b/src/desktop/mainwindow.cpp index 925350b0f..ac78ae467 100644 --- a/src/desktop/mainwindow.cpp +++ b/src/desktop/mainwindow.cpp @@ -137,6 +137,7 @@ static constexpr auto CTRL_KEY = Qt::CTRL; using desktop::settings::Settings; using std::placeholders::_1; using std::placeholders::_2; +using std::placeholders::_3; static constexpr int DEBOUNCE_MS = 250; // clang-format off @@ -477,6 +478,7 @@ MainWindow::MainWindow(bool restoreWindowPosition, bool singleSession) // Restore settings and show the window updateTitle(); readSettings(restoreWindowPosition); + setupBrushShortcuts(); // Set status indicators updateLockWidget(); @@ -1107,28 +1109,50 @@ bool MainWindow::eventFilter(QObject *object, QEvent *event) return QMainWindow::eventFilter(object, event); } +// clang-format on void MainWindow::handleAmbiguousShortcut(QShortcutEvent *shortcutEvent) { - CustomShortcutModel shortcutsModel; - shortcutsModel.loadShortcuts(dpApp().settings().shortcuts()); - const QKeySequence &keySequence = shortcutEvent->key(); - QVector shortcuts = shortcutsModel.getShortcutsMatching(keySequence); - // Shortcuts may conflict with stuff like the main window menu bar. We can - // resolve those pseudo.conflicts in the favor of our custom shortcuts. - if(shortcuts.size() == 1) { - QAction *action = findChild( - shortcuts.first().name, Qt::FindDirectChildrenOnly); - if(action) { - action->trigger(); - return; + QVector actions; + QStringList matchingShortcuts; + + { + CustomShortcutModel shortcutsModel; + shortcutsModel.loadShortcuts(dpApp().settings().shortcuts()); + for(const CustomShortcut &shortcut : + shortcutsModel.getShortcutsMatching(keySequence)) { + + QAction *action = + findChild(shortcut.name, Qt::FindDirectChildrenOnly); + if(action) { + actions.append(action); + } + + matchingShortcuts.append( + QString("
  • %1
  • ").arg(shortcut.title.toHtmlEscaped())); } } - QStringList matchingShortcuts; - for(const CustomShortcut &shortcut : shortcuts) { - matchingShortcuts.append(QString("
  • %1
  • ").arg(shortcut.title.toHtmlEscaped())); + dpApp().brushPresets()->presetModel()->getShortcutActions( + [&](const QString &name, const QString &text, + const QKeySequence &shortcut) { + if(shortcut == keySequence) { + QAction *action = searchAction(name); + if(action) { + actions.append(action); + matchingShortcuts.append( + QString("
  • %1
  • ").arg(text.toHtmlEscaped())); + } + } + }); + + // Shortcuts may conflict with stuff like the main window menu bar. We can + // resolve those pseudo.conflicts in the favor of our custom shortcuts. + if(actions.size() == 1) { + actions.first()->trigger(); + return; } + matchingShortcuts.sort(Qt::CaseInsensitive); QString message = @@ -1144,9 +1168,10 @@ void MainWindow::handleAmbiguousShortcut(QShortcutEvent *shortcutEvent) box.exec(); if(box.clickedButton() == fixButton) { - showSettings()->activateShortcutsPanel(); + showSettings()->initiateFixShortcutConflicts(); } } +// clang-format off void MainWindow::saveSplitterState() { @@ -2328,7 +2353,7 @@ void MainWindow::showBrushSettingsDialog(bool openOnPresetPage) std::function updatePreset = [brushSettings, dlg](int presetId) { bool attached = presetId > 0; - dlg->setPresetAttached(attached); + dlg->setPresetAttached(attached, presetId); if(attached) { QSignalBlocker blocker(dlg); dlg->setPresetName(brushSettings->currentPresetName()); @@ -2336,6 +2361,7 @@ void MainWindow::showBrushSettingsDialog(bool openOnPresetPage) brushSettings->currentPresetDescription()); dlg->setPresetThumbnail( brushSettings->currentPresetThumbnail()); + dlg->setPresetShortcut(brushSettings->currentPresetShortcut()); } }; connect( @@ -2385,6 +2411,19 @@ void MainWindow::showBrushSettingsDialog(bool openOnPresetPage) std::bind( &docks::BrushPalette::overwriteCurrentPreset, m_dockBrushPalette, dlg)); + connect( + dpApp().brushPresets()->presetModel(), + &brushes::BrushPresetModel::presetShortcutChanged, dlg, + [dlg](int presetId, const QKeySequence &shortcut) { + if(dlg->isPresetAttached() && dlg->presetId() == presetId) { + dlg->setPresetShortcut(shortcut); + } + }); + connect( + dlg, &dialogs::BrushSettingsDialog::shortcutChangeRequested, this, + [this](int presetId) { + showSettings()->initiateBrushShortcutChange(presetId); + }); if(openOnPresetPage) { dlg->showPresetPage(); @@ -2406,10 +2445,54 @@ dialogs::SettingsDialog *MainWindow::showSettings() dialogs::SettingsDialog *dlg = new dialogs::SettingsDialog( m_singleSession, m_smallScreenMode, getStartDialogOrThis()); dlg->setAttribute(Qt::WA_DeleteOnClose); + connect( + dlg, &dialogs::SettingsDialog::tabletTesterRequested, this, + std::bind(&MainWindow::showTabletTestDialog, this, dlg)); + connect( + dlg, &dialogs::SettingsDialog::touchTesterRequested, this, + std::bind(&MainWindow::showTouchTestDialog, this, dlg)); utils::showWindow(dlg, shouldShowDialogMaximized()); return dlg; } +dialogs::TabletTestDialog *MainWindow::showTabletTestDialog(QWidget *parent) +{ + QString name = QStringLiteral("tablettestdialog"); + dialogs::TabletTestDialog *ttd = + parent->findChild( + name, Qt::FindDirectChildrenOnly); + if(ttd) { + ttd->activateWindow(); + ttd->raise(); + } else { + ttd = new dialogs::TabletTestDialog(parent); + ttd->setWindowModality(Qt::WindowModal); + ttd->setAttribute(Qt::WA_DeleteOnClose); + ttd->setObjectName(name); + utils::showWindow(ttd, shouldShowDialogMaximized()); + } + return ttd; +} + +dialogs::TouchTestDialog *MainWindow::showTouchTestDialog(QWidget *parent) +{ + QString name = QStringLiteral("touchtestdialog"); + dialogs::TouchTestDialog *ttd = + parent->findChild( + name, Qt::FindDirectChildrenOnly); + if(ttd) { + ttd->activateWindow(); + ttd->raise(); + } else { + ttd = new dialogs::TouchTestDialog(parent); + ttd->setWindowModality(Qt::WindowModal); + ttd->setAttribute(Qt::WA_DeleteOnClose); + ttd->setObjectName(name); + utils::showWindow(ttd, shouldShowDialogMaximized()); + } + return ttd; +} + void MainWindow::host() { dialogs::StartDialog *dlg = showStartDialog(); @@ -3981,6 +4064,7 @@ ActionBuilder MainWindow::makeAction(const char *name, const QString& text) return ActionBuilder(act); } +// clang-format on QAction *MainWindow::getAction(const QString &name) { QAction *action = searchAction(name); @@ -3995,9 +4079,52 @@ QAction *MainWindow::getAction(const QString &name) QAction *MainWindow::searchAction(const QString &name) { - return findChild(name, Qt::FindDirectChildrenOnly); + return findChild(name, Qt::FindDirectChildrenOnly); } +void MainWindow::addBrushShortcut( + const QString &name, const QString &text, const QKeySequence &shortcut) +{ + Q_ASSERT(!searchAction(name)); + QAction *action = new QAction(text, this); + action->setObjectName(name); + action->setShortcut(shortcut); + addAction(action); + connect( + action, &QAction::triggered, this, + std::bind(&MainWindow::triggerBrushShortcut, this, action)); + action->installEventFilter(this); +} + +void MainWindow::changeBrushShortcut(const QString &name, const QString &text) +{ + QAction *action = searchAction(name); + if(action) { + action->setText(text); + } else { + qWarning( + "changeBrushShortcut: action '%s' not found", qUtf8Printable(name)); + } +} + +void MainWindow::removeBrushShortcut(const QString &name) +{ + QAction *action = searchAction(name); + if(action) { + removeAction(action); + delete action; + } else { + qWarning( + "removeBrushShortcut: action '%s' not found", qUtf8Printable(name)); + } +} + +void MainWindow::triggerBrushShortcut(QAction *action) +{ + m_dockBrushPalette->setSelectedPresetIdsFromShortcut(action->shortcut()); +} +// clang-format off + /** * @brief Create actions, menus and toolbars */ @@ -4414,7 +4541,7 @@ void MainWindow::setupActions() editmenu->addSeparator(); editmenu->addAction(brushSettings); #ifdef Q_OS_WIN32 - QMenu *driverMenu = editmenu->addMenu(QIcon::fromTheme("dialog-input-devices"), tr("Tablet Driver")); + QMenu *driverMenu = editmenu->addMenu(QIcon::fromTheme("input-tablet"), tr("Tablet Driver")); for(QAction *driver : drivers) { driverMenu->addAction(driver); } @@ -4466,15 +4593,45 @@ void MainWindow::setupActions() QAction *zoomoutcenter = makeAction("zoomoutcenter", tr("Zoom Out From Center")).noDefaultShortcut().autoRepeat(); QAction *zoomorig = makeAction("zoomone", tr("&Reset Zoom")).icon("zoom-original").shortcut(QKeySequence("ctrl+0")); QAction *zoomorigcenter = makeAction("zoomonecenter", tr("Reset Zoom At Center")).noDefaultShortcut(); - QAction *zoomfit = makeAction("zoomfit", tr("&Fit Page")).icon("zoom-select").noDefaultShortcut(); - QAction *zoomfitwidth = makeAction("zoomfitwidth", tr("Fit Page &Width")).icon("zoom-fit-width").noDefaultShortcut(); - QAction *zoomfitheight = makeAction("zoomfitheight", tr("Fit Page &Height")).icon("zoom-fit-height").noDefaultShortcut(); - QAction *rotateorig = makeAction("rotatezero", tr("&Reset Rotation")).icon("transform-rotate").shortcut(QKeySequence("ctrl+r")); - QAction *rotatecw = makeAction("rotatecw", tr("Rotate Canvas Clockwise")).shortcut(QKeySequence("shift+.")).icon("drawpile_rotate_right").autoRepeat(); - QAction *rotateccw = makeAction("rotateccw", tr("Rotate Canvas Counterclockwise")).shortcut(QKeySequence("shift+,")).icon("drawpile_rotate_left").autoRepeat(); + // clang-format on + QAction *zoomfit = makeAction("zoomfit", tr("&Fit Canvas")) + .icon("zoom-select") + .noDefaultShortcut(); + QAction *zoomfitwidth = makeAction("zoomfitwidth", tr("Fit Canvas &Width")) + .icon("zoom-fit-width") + .noDefaultShortcut(); + QAction *zoomfitheight = + makeAction("zoomfitheight", tr("Fit Canvas &Height")) + .icon("zoom-fit-height") + .noDefaultShortcut(); + QAction *rotateorig = makeAction("rotatezero", tr("&Reset Canvas Rotation")) + .icon("transform-rotate") + .shortcut(QKeySequence("ctrl+r")); + QAction *rotatecw = makeAction("rotatecw", tr("Rotate Canvas Clockwise")) + .shortcut(QKeySequence("shift+.")) + .icon("drawpile_rotate_right") + .autoRepeat(); + QAction *rotateccw = + makeAction("rotateccw", tr("Rotate Canvas Counter-Clockwise")) + .shortcut(QKeySequence("shift+,")) + .icon("drawpile_rotate_left") + .autoRepeat(); - QAction *viewmirror = makeAction("viewmirror", tr("Mirror")).icon("drawpile_mirror").shortcut("V").checkable(); - QAction *viewflip = makeAction("viewflip", tr("Flip")).icon("drawpile_flip").shortcut("C").checkable(); + QAction *viewmirror = + makeAction("viewmirror", tr("Mirror Canvas")) + .icon("drawpile_mirror") + .statusTip(tr("Mirror the canvas horizontally")) + .shortcutWithSearchText( + tr("mirror/flip canvas horizontally"), QKeySequence("V")) + .checkable(); + QAction *viewflip = + makeAction("viewflip", tr("Flip Canvas")) + .icon("drawpile_flip") + .statusTip(tr("Flip the canvas upside-down")) + .shortcutWithSearchText( + tr("mirror/flip canvas vertically"), QKeySequence("C")) + .checkable(); + // clang-format off QAction *showannotations = makeAction("showannotations", tr("Show &Annotations")).noDefaultShortcut().checked().remembered(); QAction *showusermarkers = makeAction("showusermarkers", tr("Show User &Pointers")).noDefaultShortcut().checked().remembered(); @@ -4724,6 +4881,7 @@ void MainWindow::setupActions() layerMenu->addAction(layerCheckAll); layerMenu->addAction(layerUncheckAll); + // clang-format on // // Select menu // @@ -4777,10 +4935,14 @@ void MainWindow::setupActions() QAction *transformmirror = makeAction("transformmirror", tr("&Mirror Transform")) .icon("drawpile_mirror") - .noDefaultShortcut(); - QAction *transformflip = makeAction("transformflip", tr("&Flip Transform")) - .icon("drawpile_flip") - .noDefaultShortcut(); + .statusTip(tr("Mirror the transformed image horizontally")) + .noDefaultShortcut( + tr("mirror/flip transformed image horizontally")); + QAction *transformflip = + makeAction("transformflip", tr("&Flip Transform")) + .icon("drawpile_flip") + .statusTip(tr("Flip the transformed image upside-down")) + .noDefaultShortcut(tr("mirror/flip transformed image vertically")); QAction *transformrotatecw = makeAction("transformrotatecw", tr("&Rotate Transform Clockwise")) .icon("drawpile_rotate_right") @@ -4791,8 +4953,7 @@ void MainWindow::setupActions() .icon("drawpile_rotate_left") .noDefaultShortcut(); QAction *transformshrinktoview = - makeAction( - "transformshrinktoview", tr("Shrink Transform to &Fit View")) + makeAction("transformshrinktoview", tr("Shrink Transform to &Fit View")) .icon("zoom-out") .noDefaultShortcut(); QAction *stamp = @@ -4868,6 +5029,7 @@ void MainWindow::setupActions() m_dockToolSettings->transformSettings()->setActions( transformmirror, transformflip, transformrotatecw, transformrotateccw, transformshrinktoview, stamp); + // clang-format off // // Animation menu @@ -5229,8 +5391,8 @@ void MainWindow::setupActions() // Help menu // QAction *homepage = makeAction("dphomepage", tr("&Homepage")).statusTip(cmake_config::website()).noDefaultShortcut(); - QAction *tablettester = makeAction("tablettester", tr("Tablet Tester")).noDefaultShortcut(); - QAction *touchtester = makeAction("touchtester", tr("Touch Tester")).noDefaultShortcut(); + QAction *tablettester = makeAction("tablettester", tr("Tablet Tester")).icon("input-tablet").noDefaultShortcut(); + QAction *touchtester = makeAction("touchtester", tr("Touch Tester")).icon("input-touchscreen").noDefaultShortcut(); QAction *showlogfile = makeAction("showlogfile", tr("Log File")).noDefaultShortcut(); QAction *about = makeAction("dpabout", tr("&About Drawpile")).menuRole(QAction::AboutRole).noDefaultShortcut(); QAction *aboutqt = makeAction("aboutqt", tr("About &Qt")).menuRole(QAction::AboutQtRole).noDefaultShortcut(); @@ -5247,37 +5409,14 @@ void MainWindow::setupActions() versioncheck, &QAction::triggered, this, &MainWindow::checkForUpdates); #endif - connect(tablettester, &QAction::triggered, [this]() { - dialogs::TabletTestDialog *ttd=nullptr; - // Check if dialog is already open - for(QWidget *toplevel : qApp->topLevelWidgets()) { - ttd = qobject_cast(toplevel); - if(ttd) - break; - } - if(!ttd) { - ttd = new dialogs::TabletTestDialog; - ttd->setAttribute(Qt::WA_DeleteOnClose); - } - utils::showWindow(ttd, shouldShowDialogMaximized()); - ttd->raise(); - }); - - connect(touchtester, &QAction::triggered, [this] { - dialogs::TouchTestDialog *ttd = nullptr; - for(QWidget *toplevel : qApp->topLevelWidgets()) { - ttd = qobject_cast(toplevel); - if(ttd) { - break; - } - } - if(!ttd) { - ttd = new dialogs::TouchTestDialog; - ttd->setAttribute(Qt::WA_DeleteOnClose); - } - utils::showWindow(ttd, shouldShowDialogMaximized()); - ttd->raise(); - }); + // clang-format on + connect( + tablettester, &QAction::triggered, this, + std::bind(&MainWindow::showTabletTestDialog, this, this)); + connect( + touchtester, &QAction::triggered, this, + std::bind(&MainWindow::showTouchTestDialog, this, this)); + // clang-format off connect(showlogfile, &QAction::triggered, [this] { QString logFilePath = utils::logFilePath(); @@ -5430,6 +5569,24 @@ void MainWindow::setupActions() updateInterfaceModeActions(); } +// clang-format on +void MainWindow::setupBrushShortcuts() +{ + brushes::BrushPresetModel *brushPresetModel = + dpApp().brushPresets()->presetModel(); + brushPresetModel->getShortcutActions( + std::bind(&MainWindow::addBrushShortcut, this, _1, _2, _3)); + connect( + brushPresetModel, &brushes::BrushPresetModel::shortcutActionAdded, this, + &MainWindow::addBrushShortcut); + connect( + brushPresetModel, &brushes::BrushPresetModel::shortcutActionChanged, + this, &MainWindow::changeBrushShortcut); + connect( + brushPresetModel, &brushes::BrushPresetModel::shortcutActionRemoved, + this, &MainWindow::removeBrushShortcut); +} + void MainWindow::updateInterfaceModeActions() { m_desktopModeActions->setEnabled(!m_smallScreenMode); @@ -5462,6 +5619,7 @@ void MainWindow::updateInterfaceModeActions() m_smallScreenEditActions.clear(); } } +// clang-format off void MainWindow::reenableUpdates() { diff --git a/src/desktop/mainwindow.h b/src/desktop/mainwindow.h index d5f6fc89c..a6e4a3cd5 100644 --- a/src/desktop/mainwindow.h +++ b/src/desktop/mainwindow.h @@ -60,6 +60,8 @@ class SessionSettingsDialog; class ServerLogDialog; class SettingsDialog; class StartDialog; +class TabletTestDialog; +class TouchTestDialog; } namespace canvas { @@ -148,6 +150,8 @@ public slots: void showFlipbook(); dialogs::SettingsDialog *showSettings(); + dialogs::TabletTestDialog *showTabletTestDialog(QWidget *parent); + dialogs::TouchTestDialog *showTouchTestDialog(QWidget *parent); void reportAbuse(); void tryToGainOp(); void resetSession(); @@ -310,6 +314,12 @@ private slots: QAction *getAction(const QString &name); QAction *searchAction(const QString &name); + void addBrushShortcut( + const QString &name, const QString &text, const QKeySequence &shortcut); + void changeBrushShortcut(const QString &name, const QString &text); + void removeBrushShortcut(const QString &name); + void triggerBrushShortcut(QAction *action); + //! Add a new entry to recent files list void addRecentFile(const QString &file); @@ -344,6 +354,7 @@ private slots: void resetDefaultDocks(); void resetDefaultToolbars(); void setupActions(); + void setupBrushShortcuts(); void updateInterfaceModeActions(); void reenableUpdates(); void keepCanvasPosition(const std::function &block); diff --git a/src/desktop/toolwidgets/brushsettings.cpp b/src/desktop/toolwidgets/brushsettings.cpp index 57d617e96..c8a59dc19 100644 --- a/src/desktop/toolwidgets/brushsettings.cpp +++ b/src/desktop/toolwidgets/brushsettings.cpp @@ -276,6 +276,7 @@ void BrushSettings::connectBrushPresets(brushes::BrushPresetModel *brushPresets) preset.originalDescription = opt->originalDescription; preset.originalThumbnail = opt->originalThumbnail; preset.originalBrush = opt->originalBrush; + preset.shortcut = opt->shortcut; preset.changedName = opt->changedName; preset.changedDescription = opt->changedDescription; preset.changedThumbnail = opt->changedThumbnail; @@ -294,6 +295,9 @@ void BrushSettings::connectBrushPresets(brushes::BrushPresetModel *brushPresets) connect( brushPresets, &brushes::BrushPresetModel::presetChanged, this, &BrushSettings::handlePresetChanged); + connect( + brushPresets, &brushes::BrushPresetModel::presetShortcutChanged, this, + &BrushSettings::handlePresetShortcutChanged); connect( brushPresets, &brushes::BrushPresetModel::presetRemoved, this, &BrushSettings::handlePresetRemoved); @@ -834,6 +838,11 @@ const QPixmap &BrushSettings::currentPresetThumbnail() const return d->currentPreset().effectiveThumbnail(); } +const QKeySequence &BrushSettings::currentPresetShortcut() const +{ + return d->currentPreset().shortcut; +} + int BrushSettings::currentBrushSlot() const { return d->current; @@ -1720,6 +1729,17 @@ void BrushSettings::handlePresetChanged( } } +void BrushSettings::handlePresetShortcutChanged( + int presetId, const QKeySequence &shortcut) +{ + for(int i = 0; i < TOTAL_SLOT_COUNT; ++i) { + Preset &preset = d->presetAt(i); + if(preset.isAttached() && preset.id == presetId) { + preset.shortcut = shortcut; + } + } +} + void BrushSettings::handlePresetRemoved(int presetId) { for(int i = 0; i < TOTAL_SLOT_COUNT; ++i) { diff --git a/src/desktop/toolwidgets/brushsettings.h b/src/desktop/toolwidgets/brushsettings.h index fe155288d..c3ba90721 100644 --- a/src/desktop/toolwidgets/brushsettings.h +++ b/src/desktop/toolwidgets/brushsettings.h @@ -65,6 +65,7 @@ class BrushSettings final : public ToolSettings { const QString ¤tPresetName() const; const QString ¤tPresetDescription() const; const QPixmap ¤tPresetThumbnail() const; + const QKeySequence ¤tPresetShortcut() const; int currentBrushSlot() const; bool isCurrentEraserSlot() const; @@ -126,6 +127,8 @@ private slots: void handlePresetChanged( int presetId, const QString &name, const QString &description, const QPixmap &thumbnail, const brushes::ActiveBrush &brush); + void + handlePresetShortcutChanged(int presetId, const QKeySequence &shortcut); void handlePresetRemoved(int presetId); void detachCurrentSlot(); diff --git a/src/desktop/update-assets-icons.sh b/src/desktop/update-assets-icons.sh index 562dd0441..df59559a8 100755 --- a/src/desktop/update-assets-icons.sh +++ b/src/desktop/update-assets-icons.sh @@ -17,7 +17,7 @@ update_icons() { folder.svg | network-server.svg | network-server-database.svg) category=places ;; - input-keyboard.svg | monitor.svg | network-modem.svg) + input-keyboard.svg | input-tablet.svg | input-touchscreen.svg | monitor.svg | network-modem.svg) category=devices ;; dialog-input-devices.svg) diff --git a/src/desktop/utils/actionbuilder.h b/src/desktop/utils/actionbuilder.h index 92c3f5ea6..e5e04ef22 100644 --- a/src/desktop/utils/actionbuilder.h +++ b/src/desktop/utils/actionbuilder.h @@ -35,14 +35,14 @@ class ActionBuilder { return *this; } - ActionBuilder &noDefaultShortcut() + ActionBuilder &noDefaultShortcut(const QString &searchText = QString()) { Q_ASSERT(!m_action->objectName().isEmpty()); Q_ASSERT(!CustomShortcutModel::isCustomizableActionRegistered( m_action->objectName())); CustomShortcutModel::registerCustomizableAction( m_action->objectName(), m_action->text().remove('&'), - m_action->icon(), QKeySequence(), QKeySequence()); + m_action->icon(), QKeySequence(), QKeySequence(), searchText); return *this; } @@ -54,6 +54,13 @@ class ActionBuilder { ActionBuilder &shortcut( const QKeySequence &shortcut, const QKeySequence &alternateShortcut = QKeySequence()) + { + return shortcutWithSearchText(QString(), shortcut, alternateShortcut); + } + + ActionBuilder &shortcutWithSearchText( + const QString &searchText, const QKeySequence &shortcut, + const QKeySequence &alternateShortcut = QKeySequence()) { Q_ASSERT(!m_action->objectName().isEmpty()); Q_ASSERT(!CustomShortcutModel::isCustomizableActionRegistered( @@ -61,7 +68,7 @@ class ActionBuilder { m_action->setShortcut(shortcut); CustomShortcutModel::registerCustomizableAction( m_action->objectName(), m_action->text().remove('&'), - m_action->icon(), shortcut, alternateShortcut); + m_action->icon(), shortcut, alternateShortcut, searchText); return *this; } diff --git a/src/libclient/CMakeLists.txt b/src/libclient/CMakeLists.txt index a2856f131..cf4535121 100644 --- a/src/libclient/CMakeLists.txt +++ b/src/libclient/CMakeLists.txt @@ -167,6 +167,8 @@ target_sources(dpclient PRIVATE utils/avatarlistmodel.cpp utils/avatarlistmodel.h utils/avatarlistmodeldelegate.h + utils/brushshortcutmodel.cpp + utils/brushshortcutmodel.h utils/canvasshortcutsmodel.cpp utils/canvasshortcutsmodel.h utils/certificatestoremodel.cpp diff --git a/src/libclient/brushes/brushpresetmodel.cpp b/src/libclient/brushes/brushpresetmodel.cpp index 448b468b2..a7a845218 100644 --- a/src/libclient/brushes/brushpresetmodel.cpp +++ b/src/libclient/brushes/brushpresetmodel.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -19,12 +20,12 @@ #include #include #include +#include #include #include namespace brushes { -static constexpr int THUMBNAIL_SIZE = 64; static constexpr int ALL_ROW = 0; static constexpr int UNTAGGED_ROW = 1; static constexpr int TAG_OFFSET = 2; @@ -59,6 +60,52 @@ struct PresetChange { std::optional thumbnail; std::optional brush; }; + +struct PresetShortcutEntry { + int id; + QString name; + + bool operator==(const PresetShortcutEntry &other) const + { + return id == other.id && name == other.name; + } + + bool operator<(const PresetShortcutEntry &other) const + { + return name.compare(other.name, Qt::CaseInsensitive) < 1; + } +}; + +struct PresetShortcut { + QVector entries; + + QVector ids() const + { + QVector v; + v.reserve(entries.size()); + for(const PresetShortcutEntry &entry : entries) { + v.append(entry.id); + } + return v; + } + + QStringList names() const + { + QStringList v; + v.reserve(entries.size()); + for(const PresetShortcutEntry &entry : entries) { + v.append(entry.name); + } + return v; + } + + QString text(const QLocale &locale) const + { + return locale.createSeparatedList(names()); + } +}; + +using PresetShortcutMap = QHash; } class BrushPresetTagModel::Private { @@ -82,6 +129,14 @@ class BrushPresetTagModel::Private { std::bind(&Private::writePresetChanges, this)); } + void setPresetModel(BrushPresetModel *presetModel) + { + Q_ASSERT(!m_presetModel); + m_presetModel = presetModel; + } + + BrushPresetModel *presetModel() const { return m_presetModel; } + int createTag(const QString &name) { DRAWPILE_FS_PERSIST_SCOPE(scopedFsSync); @@ -176,9 +231,12 @@ class BrushPresetTagModel::Private { QMutexLocker locker{&m_mutex}; QSqlQuery query(m_db); QString sql = QStringLiteral( - "insert into preset (name, description, thumbnail, type, data)\n" - " values (?, ?, ?, ?, ?)"); - if(exec(query, sql, {name, description, thumbnail, type, data})) { + "insert into preset (name, description, thumbnail, type, data) " + "values (?, ?, ?, ?, ?)"); + if(exec( + query, sql, + {nonNullString(name), nonNullString(description), thumbnail, + type, data})) { return query.lastInsertId().toInt(); } else { return 0; @@ -297,9 +355,9 @@ class BrushPresetTagModel::Private { QMutexLocker locker(&m_mutex); QSqlQuery query(m_db); QString sql = QStringLiteral( - "select id, name, description, thumbnail, data, changed_name, " - "changed_description, changed_thumbnail, changed_data " - "from preset where id = ?"); + "select id, name, description, thumbnail, data, shortcut, " + "changed_name, changed_description, changed_thumbnail, " + "changed_data from preset where id = ?"); if(exec(query, sql, {id}) && query.next()) { Preset preset; readPreset(preset, query); @@ -338,12 +396,27 @@ class BrushPresetTagModel::Private { query, QStringLiteral( "update preset set name = ?, description = ?, " - "thumbnail = ?, type = ?, data = ?, changed_name = null, " - "changed_description = null, changed_thumbnail = null, " - "changed_type = null, changed_data = null where id = ?"), - {name.isNull() ? QStringLiteral("") : name, - description.isNull() ? QStringLiteral("") : description, - thumbnail, type, data, id})) { + "thumbnail = ?, type = ?, data = ?, shortcut = ?, " + "changed_name = null, changed_description = null, " + "changed_thumbnail = null, changed_type = null, " + "changed_data = null where id = ?"), + {nonNullString(name), nonNullString(description), thumbnail, + type, data, id})) { + return query.numRowsAffected() == 1; + } else { + return false; + } + } + + bool updatePresetShortcut(int id, const QString &shortcut) + { + DRAWPILE_FS_PERSIST_SCOPE(scopedFsSync); + QMutexLocker locker(&m_mutex); + QSqlQuery query(m_db); + if(exec( + query, + QStringLiteral("update preset set shortcut = ? where id = ?"), + {nonNullString(shortcut), id})) { return query.numRowsAffected() == 1; } else { return false; @@ -567,6 +640,7 @@ class BrushPresetTagModel::Private { } } m_presetChanges.clear(); + refreshShortcutsInternal(); } } @@ -576,7 +650,102 @@ class BrushPresetTagModel::Private { m_presetChanges.clear(); } + void initShortcuts() { refreshShortcutsInternal(); } + + void refreshShortcuts() + { + QMutexLocker locker(&m_mutex); + refreshShortcutsInternal(); + } + + void getShortcutActions( + const std::function &fn) const + { + QLocale locale; + for(PresetShortcutMap::const_iterator + it = m_presetShortcuts.constBegin(), + end = m_presetShortcuts.constEnd(); + it != end; ++it) { + const QKeySequence &shortcut = it.key(); + fn(getActionName(shortcut), it->text(locale), shortcut); + } + } + + QVector getPresetIdsForShortcut(const QKeySequence &shortcut) const + { + PresetShortcutMap::const_iterator found = + m_presetShortcuts.constFind(shortcut); + if(found == m_presetShortcuts.constEnd()) { + qWarning( + "Brush shortcut for %s not found", + qUtf8Printable(shortcut.toString(QKeySequence::NativeText))); + return {}; + } else { + return found->ids(); + } + } + + bool haveShortcutForPresetId(int presetId) + { + for(const PresetShortcut &ps : m_presetShortcuts) { + for(const PresetShortcutEntry &entry : ps.entries) { + if(entry.id == presetId) { + return true; + } + } + } + return false; + } + + QVector getShortcutPresets() + { + QMutexLocker locker(&m_mutex); + QSqlQuery query(m_db); + QString sql = QStringLiteral( + "select p.id, coalesce(p.changed_name, p.name), " + "coalesce(p.changed_thumbnail, p.thumbnail), " + "p.shortcut, group_concat(pt.tag_id) from preset p " + "left join preset_tag pt on pt.preset_id = p.id " + "group by p.id, coalesce(p.changed_name, p.name), " + "coalesce(p.changed_thumbnail, p.thumbnail), p.shortcut " + "order by lower(coalesce(p.changed_name, p.name))"); + QVector results; + if(exec(query, sql)) { + while(query.next()) { + ShortcutPreset sp; + sp.id = query.value(0).toInt(); + sp.name = query.value(1).toString(); + sp.thumbnail.loadFromData(query.value(2).toByteArray()); + sp.shortcut = QKeySequence::fromString( + query.value(3).toString(), QKeySequence::PortableText); + parseGroupedTagIds(query.value(4).toString(), sp.tagIds); + results.append(sp); + } + } + return results; + } + private: + static QString nonNullString(const QString &s) + { + return s.isNull() ? QStringLiteral("") : s; + } + + static void parseGroupedTagIds(const QString &input, QVector &tagIds) + { + QStringList tagIdStrings = + input.split(QChar(','), compat::SkipEmptyParts); + tagIds.reserve(tagIdStrings.size()); + for(const QString &tagIdString : tagIdStrings) { + bool ok; + int tagId = tagIdString.toInt(&ok); + if(ok && tagId > 0) { + tagIds.append(tagId); + } + } + } + bool createOrUpdateStateInternal( QSqlQuery &query, const QString &key, const QVariant &value) { @@ -629,8 +798,8 @@ class BrushPresetTagModel::Private { QString sql = QStringLiteral( "select p.id, p.name, p.description, p.thumbnail, p.data, " - "p.changed_name, p.changed_description, p.changed_thumbnail, " - "p.changed_data, group_concat(t.tag_id) tags " + "p.shortcut, p.changed_name, p.changed_description, " + "p.changed_thumbnail, p.changed_data, group_concat(t.tag_id) tags " "from preset p left join preset_tag t on t.preset_id = p.id"); QVariantList params; switch(m_tagIdToFilter) { @@ -653,18 +822,7 @@ class BrushPresetTagModel::Private { while(query.next()) { CachedPreset cp; readPreset(cp, query); - - QStringList tagIdStrings = query.value(9).toString().split( - QChar(','), compat::SkipEmptyParts); - cp.tagIds.reserve(tagIdStrings.size()); - for(const QString &tagIdString : tagIdStrings) { - bool ok; - int tagId = tagIdString.toInt(&ok); - if(ok && tagId > 0) { - cp.tagIds.append(tagId); - } - } - + parseGroupedTagIds(query.value(10).toString(), cp.tagIds); m_presetCache.append(cp); } } @@ -685,18 +843,20 @@ class BrushPresetTagModel::Private { preset.originalBrush = loadBrush(preset.id, query.value(4).toByteArray()); + preset.shortcut = QKeySequence::fromString( + query.value(5).toString(), QKeySequence::PortableText); - QVariant changedName = query.value(5); + QVariant changedName = query.value(6); if(!changedName.isNull()) { preset.changedName = changedName.toString(); } - QVariant changedDescription = query.value(6); + QVariant changedDescription = query.value(7); if(!changedDescription.isNull()) { preset.changedDescription = changedDescription.toString(); } - QVariant changedThumbnail = query.value(7); + QVariant changedThumbnail = query.value(8); if(!changedThumbnail.isNull()) { if(pixmap.loadFromData(changedThumbnail.toByteArray())) { preset.changedThumbnail = pixmap; @@ -706,13 +866,69 @@ class BrushPresetTagModel::Private { } } - QVariant changedData = query.value(8); + QVariant changedData = query.value(9); if(!changedData.isNull()) { preset.changedBrush = loadBrush(preset.id, changedData.toByteArray()); } } + static QString getActionName(const QKeySequence &shortcut) + { + return QStringLiteral("__brushshortcut_%1") + .arg(shortcut.toString(QKeySequence::PortableText)); + } + + void refreshShortcutsInternal() + { + Q_ASSERT(m_presetModel); + PresetShortcutMap newPresetShortcuts; + + QSqlQuery query(m_db); + QString sql = + QStringLiteral("select shortcut, id, coalesce(changed_name, name) " + "from preset where coalesce(shortcut, '') <> ''"); + if(exec(query, sql)) { + while(query.next()) { + QKeySequence shortcut = QKeySequence::fromString( + query.value(0).toString(), QKeySequence::PortableText); + if(!shortcut.isEmpty()) { + newPresetShortcuts[shortcut].entries.append( + {query.value(1).toInt(), query.value(2).toString()}); + } + } + } + + QLocale locale; + for(PresetShortcutMap::iterator it = newPresetShortcuts.begin(), + end = newPresetShortcuts.end(); + it != end; ++it) { + + std::sort(it->entries.begin(), it->entries.end()); + QString text = locale.createSeparatedList(it->names()); + const QKeySequence &shortcut = it.key(); + + PresetShortcutMap::iterator found = + m_presetShortcuts.find(shortcut); + if(found == m_presetShortcuts.end()) { + emit m_presetModel->shortcutActionAdded( + getActionName(shortcut), text, shortcut); + } else { + if(found->entries != it->entries) { + emit m_presetModel->shortcutActionChanged( + getActionName(shortcut), text); + } + m_presetShortcuts.erase(found); + } + } + + for(QKeySequence &shortcut : m_presetShortcuts.keys()) { + emit m_presetModel->shortcutActionRemoved(getActionName(shortcut)); + } + + m_presetShortcuts.swap(newPresetShortcuts); + } + int readInt( const QString &sql, const QList ¶ms = {}, int defaultValue = 0) @@ -807,6 +1023,7 @@ class BrushPresetTagModel::Private { QVector> migrations = { &Private::migrateInitial, &Private::migrateChangedPreset, + &Private::migratePresetShortcuts, }; int originalMigrationVersion = migrationVersionVariant.toInt(); @@ -879,27 +1096,49 @@ class BrushPresetTagModel::Private { "alter table preset add column changed_data blob")); } + bool migratePresetShortcuts(QSqlQuery &query) + { + return exec( + query, QStringLiteral("alter table preset add column " + "shortcut text not null default ''")); + } + QRecursiveMutex m_mutex; QSqlDatabase m_db; QTimer m_presetChangeTimer; QHash m_presetChanges; QVector m_tagCache; QVector m_presetCache; - int m_presetIconSize = THUMBNAIL_SIZE; + PresetShortcutMap m_presetShortcuts; + BrushPresetModel *m_presetModel = nullptr; + int m_presetIconSize = BrushPresetModel::THUMBNAIL_SIZE; int m_tagIdToFilter = -1; }; +bool Tag::accepts(const QVector &tagIds) const +{ + switch(id) { + case ALL_ID: + return true; + case UNTAGGED_ID: + return tagIds.isEmpty(); + default: + return tagIds.contains(id); + } +} + BrushPresetTagModel::BrushPresetTagModel(QObject *parent) : QAbstractItemModel(parent) , d(new Private) - , m_presetModel(new BrushPresetModel(this)) { + d->setPresetModel(new BrushPresetModel(this)); maybeConvertOldPresets(); + d->initShortcuts(); connect( - this, &QAbstractItemModel::modelAboutToBeReset, m_presetModel, + this, &QAbstractItemModel::modelAboutToBeReset, d->presetModel(), &BrushPresetModel::tagsAboutToBeReset); connect( - this, &QAbstractItemModel::modelReset, m_presetModel, + this, &QAbstractItemModel::modelReset, d->presetModel(), &BrushPresetModel::tagsReset); } @@ -908,6 +1147,11 @@ BrushPresetTagModel::~BrushPresetTagModel() delete d; } +BrushPresetModel *BrushPresetTagModel::presetModel() +{ + return d->presetModel(); +} + int BrushPresetTagModel::rowCount(const QModelIndex &parent) const { return parent.isValid() ? 0 : d->tagCacheSize() + TAG_OFFSET; @@ -1186,7 +1430,7 @@ void BrushPresetTagModel::convertOldPresets() QString description = tr("Converted from %1.").arg(preset.filename); brush.setClassic(preset.brush); - std::optional opt = m_presetModel->newPreset( + std::optional opt = d->presetModel()->newPreset( name, description, brush.presetThumbnail(), brush, tagId); } } @@ -1205,7 +1449,7 @@ BrushImportResult BrushPresetTagModel::importBrushPack(const QString &file) } beginResetModel(); - m_presetModel->beginResetModel(); + d->presetModel()->beginResetModel(); QVector groups = readOrderConf(result, file, zr); for(const ImportBrushGroup &group : groups) { @@ -1216,7 +1460,7 @@ BrushImportResult BrushPresetTagModel::importBrushPack(const QString &file) d->refreshTagCache(); d->refreshPresetCache(); - m_presetModel->endResetModel(); + d->presetModel()->endResetModel(); endResetModel(); return result; } @@ -1411,11 +1655,11 @@ bool BrushPresetTagModel::readImportBrush( outThumbnail = outBrush.presetThumbnail(); } - if(outThumbnail.width() != THUMBNAIL_SIZE || - outThumbnail.height() != THUMBNAIL_SIZE) { + if(outThumbnail.width() != BrushPresetModel::THUMBNAIL_SIZE || + outThumbnail.height() != BrushPresetModel::THUMBNAIL_SIZE) { outThumbnail = outThumbnail.scaled( - THUMBNAIL_SIZE, THUMBNAIL_SIZE, Qt::IgnoreAspectRatio, - Qt::SmoothTransformation); + BrushPresetModel::THUMBNAIL_SIZE, BrushPresetModel::THUMBNAIL_SIZE, + Qt::IgnoreAspectRatio, Qt::SmoothTransformation); } return true; @@ -1966,6 +2210,21 @@ std::optional BrushPresetModel::searchPresetBrushData(int presetId) } } +QPixmap BrushPresetModel::searchPresetThumbnail(int presetId) +{ + int i = d->getCachedPresetIndexById(presetId); + if(i == -1) { + QPixmap pixmap; + if(pixmap.loadFromData(d->readPresetEffectiveThumbnailById(presetId))) { + return pixmap; + } else { + return QPixmap(); + } + } else { + return d->getCachedPreset(i).effectiveThumbnail(); + } +} + bool BrushPresetModel::changeTagAssignment( int presetId, int tagId, bool assigned) { @@ -2018,9 +2277,12 @@ std::optional BrushPresetModel::newPreset( } d->refreshPresetCache(); endResetModel(); + d->refreshShortcuts(); if(presetId > 0) { - return Preset{presetId, name, description, thumbnail, brush, - {}, {}, {}, {}}; + return Preset{ + presetId, name, description, thumbnail, brush, + QKeySequence(), {}, {}, {}, {}, + }; } else { return {}; } @@ -2037,10 +2299,35 @@ bool BrushPresetModel::updatePreset( brush.presetData()); d->refreshPresetCache(); endResetModel(); + d->refreshShortcuts(); emit presetChanged(presetId, name, description, thumbnail, brush); return ok; } +bool BrushPresetModel::updatePresetShortcut( + int presetId, const QKeySequence &shortcut) +{ + bool ok = d->updatePresetShortcut( + presetId, shortcut.toString(QKeySequence::PortableText)); + + int count = d->presetCacheSize(); + for(int i = 0; i < count; ++i) { + CachedPreset &cp = d->getCachedPreset(i); + if(cp.id == presetId) { + if(cp.shortcut != shortcut) { + cp.shortcut = shortcut; + QModelIndex idx = createIndex(i, 0); + emit dataChanged(idx, idx); + break; + } + } + } + + d->refreshShortcuts(); + emit presetShortcutChanged(presetId, shortcut); + return ok; +} + bool BrushPresetModel::deletePreset(int presetId) { bool ok = d->deletePresetById(presetId); @@ -2050,6 +2337,7 @@ bool BrushPresetModel::deletePreset(int presetId) d->removeCachedPreset(i); endRemoveRows(); } + d->refreshShortcuts(); emit presetRemoved(presetId); return ok; } @@ -2083,6 +2371,7 @@ void BrushPresetModel::resetAllPresetChanges() d->resetAllPresetChanges(); d->resetAllPresetsInCache(); endResetModel(); + d->refreshShortcuts(); } void BrushPresetModel::writePresetChanges() @@ -2095,6 +2384,24 @@ int BrushPresetModel::countNames(const QString &name) const return d->readPresetCountByName(name); } +void BrushPresetModel::getShortcutActions( + const std::function< + void(const QString &, const QString &, const QKeySequence &)> &fn) const +{ + d->getShortcutActions(fn); +} + +QVector +BrushPresetModel::getPresetIdsForShortcut(const QKeySequence &shortcut) const +{ + return d->getPresetIdsForShortcut(shortcut); +} + +QVector BrushPresetModel::getShortcutPresets() const +{ + return d->getShortcutPresets(); +} + QSize BrushPresetModel::iconSize() const { int dimension = iconDimension(); diff --git a/src/libclient/brushes/brushpresetmodel.h b/src/libclient/brushes/brushpresetmodel.h index 80d3be62f..5788f8d5a 100644 --- a/src/libclient/brushes/brushpresetmodel.h +++ b/src/libclient/brushes/brushpresetmodel.h @@ -4,7 +4,9 @@ #include "libclient/brushes/brush.h" #include #include +#include #include +#include #include class QFile; @@ -27,6 +29,7 @@ struct Tag { bool isAssignable() const { return id > 0; } bool isEditable() const { return id > 0; } + bool accepts(const QVector &tagIds) const; }; struct TagAssignment { @@ -41,6 +44,7 @@ struct Preset { QString originalDescription; QPixmap originalThumbnail; ActiveBrush originalBrush; + QKeySequence shortcut; std::optional changedName; std::optional changedDescription; std::optional changedThumbnail; @@ -75,6 +79,14 @@ struct Preset { } }; +struct ShortcutPreset { + int id; + QString name; + QPixmap thumbnail; + QKeySequence shortcut; + QVector tagIds; +}; + struct PresetMetadata { int id; QString name; @@ -109,7 +121,7 @@ class BrushPresetTagModel final : public QAbstractItemModel { explicit BrushPresetTagModel(QObject *parent = nullptr); ~BrushPresetTagModel() override; - BrushPresetModel *presetModel() { return m_presetModel; } + BrushPresetModel *presetModel(); int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; @@ -204,7 +216,6 @@ class BrushPresetTagModel final : public QAbstractItemModel { class Private; Private *d; - BrushPresetModel *m_presetModel; }; class BrushPresetModel final : public QAbstractItemModel { @@ -212,6 +223,8 @@ class BrushPresetModel final : public QAbstractItemModel { friend class BrushPresetTagModel; public: + static constexpr int THUMBNAIL_SIZE = 64; + enum Roles { FilterRole = Qt::UserRole + 1, IdRole, @@ -248,6 +261,7 @@ class BrushPresetModel final : public QAbstractItemModel { bool changeTagAssignment(int presetId, int tagId, bool assigned); std::optional searchPresetBrushData(int presetId); + QPixmap searchPresetThumbnail(int presetId); std::optional newPreset( const QString &name, const QString description, @@ -257,6 +271,8 @@ class BrushPresetModel final : public QAbstractItemModel { int presetId, const QString &name, const QString &description, const QPixmap &thumbnail, const ActiveBrush &brush); + bool updatePresetShortcut(int presetId, const QKeySequence &shortcut); + bool deletePreset(int presetId); void changePreset( @@ -270,6 +286,14 @@ class BrushPresetModel final : public QAbstractItemModel { int countNames(const QString &name) const; + void getShortcutActions( + const std::function &fn) const; + + QVector getPresetIdsForShortcut(const QKeySequence &shortcut) const; + + QVector getShortcutPresets() const; + QSize iconSize() const; int iconDimension() const; void setIconDimension(int dimension); @@ -282,7 +306,12 @@ public slots: void presetChanged( int presetId, const QString &name, const QString &description, const QPixmap &thumbnail, const ActiveBrush &brush); + void presetShortcutChanged(int presetId, const QKeySequence &shortcut); void presetRemoved(int presetId); + void shortcutActionAdded( + const QString &name, const QString &text, const QKeySequence &shortcut); + void shortcutActionChanged(const QString &name, const QString &text); + void shortcutActionRemoved(const QString &name); private: static QPixmap loadBrushPreview(const QFileInfo &fileInfo); diff --git a/src/libclient/canvas/canvasshortcuts.cpp b/src/libclient/canvas/canvasshortcuts.cpp index 8812b6f33..d62c4d20a 100644 --- a/src/libclient/canvas/canvasshortcuts.cpp +++ b/src/libclient/canvas/canvasshortcuts.cpp @@ -1,20 +1,16 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #include "libclient/canvas/canvasshortcuts.h" - #include #include -CanvasShortcuts::CanvasShortcuts() - : m_shortcuts{} -{ -} +CanvasShortcuts::CanvasShortcuts() {} CanvasShortcuts CanvasShortcuts::load(const QVariantMap &cfg) { - const QVector shortcuts = shortcutsToList(cfg.value("shortcuts")); + const QVector shortcuts = + shortcutsToList(cfg.value("shortcuts")); CanvasShortcuts cs; - if (shortcuts.isEmpty() && !cfg.value("defaultsloaded").toBool()) { + if(shortcuts.isEmpty() && !cfg.value("defaultsloaded").toBool()) { cs.loadDefaults(); } else { for(const auto &shortcut : shortcuts) { @@ -204,7 +200,7 @@ void CanvasShortcuts::clear() QVariantMap CanvasShortcuts::save() const { - QVariantMap map = { {"defaultsloaded", true} }; + QVariantMap map = {{"defaultsloaded", true}}; QVariantList shortcuts; int count = m_shortcuts.size(); for(int i = 0; i < count; ++i) { @@ -242,8 +238,8 @@ int CanvasShortcuts::addShortcut(const Shortcut &s) m_shortcuts.append(s); return index; } else { - qWarning() << "Not adding invalid canvas shortcut" - << s.type << s.mods << s.keys << s.button << s.action << s.flags; + qWarning() << "Not adding invalid canvas shortcut" << s.type << s.mods + << s.keys << s.button << s.action << s.flags; return -1; } } @@ -398,6 +394,19 @@ bool CanvasShortcuts::Shortcut::isUnmodifiedClick( mods == Qt::NoModifier && keys.isEmpty(); } +QSet CanvasShortcuts::Shortcut::keySequences() const +{ + QSet set; + for(Qt::Key key : keys) { + if(mods == Qt::NoModifier) { + set.insert(QKeySequence(key)); + } else { + set.insert(QKeySequence(mods | key)); + } + } + return set; +} + int CanvasShortcuts::searchShortcutIndex(const Shortcut &s) const { for(int i = 0; i < m_shortcuts.size(); ++i) { diff --git a/src/libclient/canvas/canvasshortcuts.h b/src/libclient/canvas/canvasshortcuts.h index c8f12fda8..1311774f4 100644 --- a/src/libclient/canvas/canvasshortcuts.h +++ b/src/libclient/canvas/canvasshortcuts.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later #ifndef CANVASSHORTCUTS_H #define CANVASSHORTCUTS_H - +#include #include #include #include @@ -61,6 +61,8 @@ class CanvasShortcuts { bool isValid(bool checkAction = true) const; bool isUnmodifiedClick(Qt::MouseButton inButton) const; + + QSet keySequences() const; }; struct Match { diff --git a/src/libclient/utils/brushshortcutmodel.cpp b/src/libclient/utils/brushshortcutmodel.cpp new file mode 100644 index 000000000..a8db1c2aa --- /dev/null +++ b/src/libclient/utils/brushshortcutmodel.cpp @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "libclient/utils/brushshortcutmodel.h" +#include +#include + +using std::placeholders::_1; +using std::placeholders::_2; +using std::placeholders::_3; + + +BrushShortcutModel::BrushShortcutModel( + brushes::BrushPresetModel *presetModel, QSize iconSize, QObject *parent) + : QAbstractTableModel(parent) + , m_presetModel(presetModel) + , m_iconSize(iconSize) + , m_presets(m_presetModel->getShortcutPresets()) +{ + for(brushes::ShortcutPreset &sp : m_presets) { + if(!sp.thumbnail.isNull() && sp.thumbnail.size() != m_iconSize) { + sp.thumbnail = sp.thumbnail.scaled( + m_iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + } + } + updateConflictRows(false); +} + +int BrushShortcutModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : m_presets.size(); +} + +int BrushShortcutModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : int(ColumnCount); +} + +QVariant BrushShortcutModel::data(const QModelIndex &index, int role) const +{ + if(!index.isValid()) { + return QVariant(); + } + + int row = index.row(); + if(row < 0 || row > m_presets.size()) { + return QVariant(); + } + + switch(role) { + case Qt::DisplayRole: + case Qt::ToolTipRole: { + QString text; + switch(index.column()) { + case int(PresetName): + text = m_presets[row].name; + break; + case int(Shortcut): + text = m_presets[row].shortcut.toString(QKeySequence::NativeText); + break; + default: + break; + } + if(role == Qt::ToolTipRole && m_conflictRows.contains(row)) { + if(text.isEmpty()) { + //: Tooltip for a keyboard shortcut conflict. + return tr("Conflict"); + } else { + //: Tooltip for a keyboard shortcut conflict, %1 is the name or + //: key sequence of the shortcut in question. + return tr("%1 (conflict)").arg(text); + } + } else { + return text; + } + } + case Qt::EditRole: + switch(index.column()) { + case int(PresetName): + return m_presets[row].name; + case int(Shortcut): + return m_presets[row].shortcut; + default: + return QVariant(); + } + case Qt::DecorationRole: + switch(index.column()) { + case int(PresetName): + return m_presets[row].thumbnail; + case int(Shortcut): + if(m_conflictRows.contains(row)) { + return QIcon::fromTheme("dialog-warning"); + } else { + return QVariant(); + } + default: + return QVariant(); + } + case Qt::TextAlignmentRole: + switch(index.column()) { + case int(Shortcut): + return Qt::AlignCenter; + default: + return QVariant(); + } + case Qt::ForegroundRole: + if(m_conflictRows.contains(row)) { + return QColor(Qt::white); + } else { + return QVariant(); + } + case Qt::BackgroundRole: + if(m_conflictRows.contains(row)) { + return QColor(0xdc3545); + } else { + return QVariant(); + } + case int(FilterRole): { + const brushes::ShortcutPreset &sp = m_presets[row]; + QString conflictMarker = + m_conflictRows.contains(row) ? QStringLiteral("\n\1") : QString(); + return QStringLiteral("action:%1\nshortcut:%2%3") + .arg( + sp.name, sp.shortcut.toString(QKeySequence::NativeText), + conflictMarker); + } + default: + return QVariant(); + } +} + +bool BrushShortcutModel::setData( + const QModelIndex &index, const QVariant &value, int role) +{ + if(index.isValid() && role == Qt::EditRole && + index.column() == int(Shortcut)) { + int row = index.row(); + if(row >= 0 && row < m_presets.size()) { + brushes::ShortcutPreset &sp = m_presets[row]; + sp.shortcut = value.value(); + m_presetModel->updatePresetShortcut(sp.id, sp.shortcut); + emit dataChanged(index, index); + updateConflictRows(true); + return true; + } + } + return false; +} + +QVariant BrushShortcutModel::headerData( + int section, Qt::Orientation orientation, int role) const +{ + if(role == Qt::DisplayRole && orientation == Qt::Horizontal) { + switch(section) { + case int(PresetName): + return tr("Brush"); + case int(Shortcut): + return tr("Shortcut"); + default: + break; + } + } + return QVariant(); +} + +Qt::ItemFlags BrushShortcutModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags flags = Qt::ItemIsSelectable | Qt::ItemIsEnabled; + int column = index.column(); + if(column == int(Shortcut)) { + flags |= Qt::ItemIsEditable; + } + return flags; +} + +const QKeySequence &BrushShortcutModel::shortcutAt(int row) +{ + Q_ASSERT(row >= 0); + Q_ASSERT(row < m_presets.size()); + return m_presets[row].shortcut; +} + +const QVector &BrushShortcutModel::tagIdsAt(int row) +{ + Q_ASSERT(row >= 0); + Q_ASSERT(row < m_presets.size()); + return m_presets[row].tagIds; +} + +QModelIndex BrushShortcutModel::indexById(int presetId, int column) const +{ + int count = m_presets.size(); + for(int i = 0; i < count; ++i) { + if(m_presets[i].id == presetId) { + return createIndex(i, column); + } + } + return QModelIndex(); +} + +void BrushShortcutModel::setExternalKeySequences( + const QSet &externalKeySequences) +{ + if(externalKeySequences != m_externalKeySequences) { + m_externalKeySequences = externalKeySequences; + updateConflictRows(true); + } +} + +void BrushShortcutModel::updateConflictRows(bool emitDataChanges) +{ + QSet conflictRows; + int rowCount = m_presets.size(); + for(int i = 0; i < rowCount; ++i) { + if(m_externalKeySequences.contains(m_presets[i].shortcut)) { + conflictRows.insert(i); + } + } + + if(emitDataChanges) { + for(int row : conflictRows + m_conflictRows) { + if(row >= 0 && + (!conflictRows.contains(row) || !m_conflictRows.contains(row))) { + emit dataChanged( + createIndex(row, 0), createIndex(row, ColumnCount - 1)); + } + } + } + + m_conflictRows.swap(conflictRows); +} + + +BrushShortcutFilterProxyModel::BrushShortcutFilterProxyModel( + brushes::BrushPresetTagModel *tagModel, QObject *parent) + : QSortFilterProxyModel(parent) + , m_tagModel(tagModel) +{ +} + +void BrushShortcutFilterProxyModel::setCurrentTagRow(int tagRow) +{ + if(tagRow != m_tagRow) { + m_tagRow = tagRow; + invalidateFilter(); + } +} + +void BrushShortcutFilterProxyModel::setSearchAllTags(bool searchAllTags) +{ + if(searchAllTags != m_searchAllTags) { + m_searchAllTags = searchAllTags; + if(m_haveSearch) { + invalidateFilter(); + } + } +} + +void BrushShortcutFilterProxyModel::setSearchString(const QString &searchString) +{ + QString s = searchString.trimmed(); + m_haveSearch = !s.isEmpty(); + setFilterFixedString(s); +} + +bool BrushShortcutFilterProxyModel::filterAcceptsRow( + int sourceRow, const QModelIndex &sourceParent) const +{ + if(m_searchAllTags && m_haveSearch) { + return QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent); + } else { + BrushShortcutModel *model = + qobject_cast(sourceModel()); + return model && !sourceParent.isValid() && + m_tagModel->getTagAt(m_tagRow).accepts( + model->tagIdsAt(sourceRow)) && + QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent); + } +} diff --git a/src/libclient/utils/brushshortcutmodel.h b/src/libclient/utils/brushshortcutmodel.h new file mode 100644 index 000000000..ad0efa0eb --- /dev/null +++ b/src/libclient/utils/brushshortcutmodel.h @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#ifndef LIBCLIENT_UTILS_BRUSHSHORTCUTMODEL_H +#define LIBCLIENT_UTILS_BRUSHSHORTCUTMODEL_H +#include "libclient/brushes/brushpresetmodel.h" +#include +#include +#include +#include +#include +#include + +namespace brushes { +class BrushPresetModel; +} + +class BrushShortcutModel final : public QAbstractTableModel { + Q_OBJECT +public: + enum Column { PresetName = 0, Shortcut, ColumnCount }; + enum Role { FilterRole = Qt::UserRole + 1 }; + + explicit BrushShortcutModel( + brushes::BrushPresetModel *presetModel, QSize iconSize, + QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + + QVariant + data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + bool + setData(const QModelIndex &index, const QVariant &value, int role) override; + + QVariant headerData( + int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const override; + + Qt::ItemFlags flags(const QModelIndex &index) const override; + + const QKeySequence &shortcutAt(int row); + const QVector &tagIdsAt(int row); + + QModelIndex indexById(int presetId, int column = 0) const; + + bool hasConflicts() const { return !m_conflictRows.isEmpty(); } + + void + setExternalKeySequences(const QSet &externalKeySequences); + +private: + void updateConflictRows(bool emitDataChanges); + + brushes::BrushPresetModel *m_presetModel; + QSet m_conflictRows; + QSet m_externalKeySequences; + QSize m_iconSize; + QVector m_presets; +}; + +// Note: Do not use setFilterFixedString with this model, use setSearchString +// instead. QSortFilterProxyModel does not expose the filter string properly. +class BrushShortcutFilterProxyModel : public QSortFilterProxyModel { + Q_OBJECT +public: + BrushShortcutFilterProxyModel( + brushes::BrushPresetTagModel *tagModel, QObject *parent = nullptr); + + void setCurrentTagRow(int tagRow); + void setSearchAllTags(bool searchAllTags); + void setSearchString(const QString &searchString); + +protected: + bool filterAcceptsRow( + int sourceRow, const QModelIndex &sourceParent) const override; + +private: + brushes::BrushPresetTagModel *m_tagModel; + int m_tagRow = 0; + bool m_searchAllTags = false; + bool m_haveSearch = false; +}; + +#endif diff --git a/src/libclient/utils/canvasshortcutsmodel.cpp b/src/libclient/utils/canvasshortcutsmodel.cpp index 18ce943b0..e740206ff 100644 --- a/src/libclient/utils/canvasshortcutsmodel.cpp +++ b/src/libclient/utils/canvasshortcutsmodel.cpp @@ -1,12 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "libclient/utils/canvasshortcutsmodel.h" +#include #include +#include #include CanvasShortcutsModel::CanvasShortcutsModel(QObject *parent) - : QAbstractTableModel{parent} - , m_canvasShortcuts{} - , m_hasChanges{false} + : QAbstractTableModel(parent) { } @@ -14,6 +14,7 @@ void CanvasShortcutsModel::loadShortcuts(const QVariantMap &cfg) { beginResetModel(); m_canvasShortcuts = CanvasShortcuts::load(cfg); + updateConflictRows(false); m_hasChanges = false; endResetModel(); } @@ -28,6 +29,7 @@ void CanvasShortcutsModel::restoreDefaults() beginResetModel(); m_canvasShortcuts.clear(); m_canvasShortcuts.loadDefaults(); + updateConflictRows(false); m_hasChanges = true; endResetModel(); } @@ -44,34 +46,96 @@ int CanvasShortcutsModel::columnCount(const QModelIndex &parent) const QVariant CanvasShortcutsModel::data(const QModelIndex &index, int role) const { - if (!index.isValid() - || index.parent().isValid() - || (role != Qt::DisplayRole && role != Qt::ToolTipRole) - ) { + if(!index.isValid() || index.parent().isValid()) { return QVariant(); } - const auto *s = shortcutAt(index.row()); - if (!s) { + int row = index.row(); + const CanvasShortcuts::Shortcut *s = shortcutAt(row); + if(!s) { return QVariant(); } - switch(Column(index.column())) { - case Shortcut: - return shortcutToString(s->type, s->mods, s->keys, s->button); - case Action: - return actionToString(*s); - case Modifiers: - return flagsToString(*s); - case ColumnCount: {} + switch(role) { + case Qt::DisplayRole: + case Qt::ToolTipRole: { + QString text; + switch(index.column()) { + case int(Shortcut): + text = shortcutToString(s->type, s->mods, s->keys, s->button); + break; + case int(Action): + text = actionToString(*s); + break; + case int(Modifiers): + text = flagsToString(*s); + break; + default: + break; + } + if(role == Qt::ToolTipRole && m_conflictRows.contains(row)) { + if(text.isEmpty()) { + //: Tooltip for a keyboard shortcut conflict. + return tr("Conflict"); + } else { + //: Tooltip for a keyboard shortcut conflict, %1 is the name or + //: key sequence of the shortcut in question. + return tr("%1 (conflict)").arg(text); + } + } else { + return text; + } + } + case Qt::DecorationRole: + switch(index.column()) { + case int(Shortcut): + if(m_conflictRows.contains(row)) { + return QIcon::fromTheme("dialog-warning"); + } else { + return QVariant(); + } + default: + return QVariant(); + } + case Qt::TextAlignmentRole: + switch(index.column()) { + case int(Shortcut): + case int(Modifiers): + return Qt::AlignCenter; + default: + return QVariant(); + } + case Qt::ForegroundRole: + if(m_conflictRows.contains(row)) { + return QColor(Qt::white); + } else { + return QVariant(); + } + case Qt::BackgroundRole: + if(m_conflictRows.contains(row)) { + return QColor(0xdc3545); + } else { + return QVariant(); + } + case int(FilterRole): { + QString conflictMarker = + m_conflictRows.contains(row) ? QStringLiteral("\n\1") : QString(); + return QStringLiteral("action:%1\nshortcut:%2%3") + .arg( + actionToString(*s), + shortcutToString(s->type, s->mods, s->keys, s->button), + conflictMarker); + } + default: + return QVariant(); } - return QVariant(); } bool CanvasShortcutsModel::removeRows( int row, int count, const QModelIndex &parent) { - if (parent.isValid() || count <= 0 || row < 0 || row + count > m_canvasShortcuts.shortcutsCount()) { + if(parent.isValid() || count <= 0 || row < 0 || + row + count > m_canvasShortcuts.shortcutsCount()) { return false; } @@ -79,13 +143,14 @@ bool CanvasShortcutsModel::removeRows( m_canvasShortcuts.removeShortcutAt(row, count); m_hasChanges = true; endRemoveRows(); + updateConflictRows(true); return true; } QVariant CanvasShortcutsModel::headerData( int section, Qt::Orientation orientation, int role) const { - if (role != Qt::DisplayRole || orientation != Qt::Horizontal) { + if(role != Qt::DisplayRole || orientation != Qt::Horizontal) { return QVariant(); } @@ -96,9 +161,10 @@ QVariant CanvasShortcutsModel::headerData( return tr("Action"); case Modifiers: return tr("Modifiers"); - case ColumnCount: {} + case ColumnCount: + break; } - return QVariant{}; + return QVariant(); } Qt::ItemFlags CanvasShortcutsModel::flags(const QModelIndex &) const @@ -111,14 +177,16 @@ const CanvasShortcuts::Shortcut *CanvasShortcutsModel::shortcutAt(int row) const return m_canvasShortcuts.shortcutAt(row); } -QModelIndex CanvasShortcutsModel::addShortcut(const CanvasShortcuts::Shortcut &s) +QModelIndex +CanvasShortcutsModel::addShortcut(const CanvasShortcuts::Shortcut &s) { if(!s.isValid()) { return QModelIndex(); } beginResetModel(); - const auto row = m_canvasShortcuts.addShortcut(s); + int row = m_canvasShortcuts.addShortcut(s); + updateConflictRows(false); m_hasChanges = true; endResetModel(); return createIndex(row, 0); @@ -132,7 +200,8 @@ QModelIndex CanvasShortcutsModel::editShortcut( } beginResetModel(); - const auto row = m_canvasShortcuts.editShortcut(prev, s); + int row = m_canvasShortcuts.editShortcut(prev, s); + updateConflictRows(false); m_hasChanges = true; endResetModel(); return createIndex(row, 0); @@ -148,7 +217,7 @@ const CanvasShortcuts::Shortcut *CanvasShortcutsModel::searchConflict( QString CanvasShortcutsModel::shortcutTitle( const CanvasShortcuts::Shortcut *s, bool actionAndFlagsOnly) { - if (!s) { + if(!s) { return QString(); } @@ -224,9 +293,37 @@ QString CanvasShortcutsModel::shortcutToString( return components.join(tr("+")); } -bool CanvasShortcutsModel::hasChanges() const +void CanvasShortcutsModel::setExternalKeySequences( + const QSet &externalKeySequences) +{ + if(externalKeySequences != m_externalKeySequences) { + m_externalKeySequences = externalKeySequences; + updateConflictRows(true); + } +} + +void CanvasShortcutsModel::updateConflictRows(bool emitDataChanges) { - return m_hasChanges; + QSet conflictRows; + int rowCount = m_canvasShortcuts.shortcutsCount(); + for(int i = 0; i < rowCount; ++i) { + const CanvasShortcuts::Shortcut *s = shortcutAt(i); + if(s && s->keySequences().intersects(m_externalKeySequences)) { + conflictRows.insert(i); + } + } + + if(emitDataChanges) { + for(int row : conflictRows + m_conflictRows) { + if(row >= 0 && + (!conflictRows.contains(row) || !m_conflictRows.contains(row))) { + emit dataChanged( + createIndex(row, 0), createIndex(row, ColumnCount - 1)); + } + } + } + + m_conflictRows.swap(conflictRows); } QString CanvasShortcutsModel::mouseButtonToString(Qt::MouseButton button) diff --git a/src/libclient/utils/canvasshortcutsmodel.h b/src/libclient/utils/canvasshortcutsmodel.h index 3114f0fac..f2491192c 100644 --- a/src/libclient/utils/canvasshortcutsmodel.h +++ b/src/libclient/utils/canvasshortcutsmodel.h @@ -1,21 +1,19 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#ifndef CANVASSHORTCUTSMODEL_H -#define CANVASSHORTCUTSMODEL_H - +#ifndef LIBCLIENT_UTILS_CANVASSHORTCUTSMODEL_H +#define LIBCLIENT_UTILS_CANVASSHORTCUTSMODEL_H #include "libclient/canvas/canvasshortcuts.h" #include -#include +#include +#include #include +#include class CanvasShortcutsModel final : public QAbstractTableModel { Q_OBJECT public: - enum Column { - Action = 0, - Shortcut, - Modifiers, - ColumnCount - }; + enum Column { Action = 0, Shortcut, Modifiers, ColumnCount }; + + enum Role { FilterRole = Qt::UserRole + 1 }; explicit CanvasShortcutsModel(QObject *parent = nullptr); @@ -26,9 +24,11 @@ class CanvasShortcutsModel final : public QAbstractTableModel { int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QVariant + data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; + bool removeRows( + int row, int count, const QModelIndex &parent = QModelIndex()) override; QVariant headerData( int section, Qt::Orientation orientation, @@ -49,22 +49,29 @@ class CanvasShortcutsModel final : public QAbstractTableModel { const CanvasShortcuts::Shortcut *except) const; static QString shortcutTitle( - const CanvasShortcuts::Shortcut *s, - bool actionAndFlagsOnly = false); + const CanvasShortcuts::Shortcut *s, bool actionAndFlagsOnly = false); static QString shortcutToString( unsigned int type, Qt::KeyboardModifiers mods, const QSet &keys, Qt::MouseButton button); - bool hasChanges() const; + bool hasChanges() const { return m_hasChanges; } + bool hasConflicts() const { return !m_conflictRows.isEmpty(); } + + void + setExternalKeySequences(const QSet &externalKeySequences); private: + void updateConflictRows(bool emitDataChanges); + static QString mouseButtonToString(Qt::MouseButton button); static QString actionToString(const CanvasShortcuts::Shortcut &s); static QString flagsToString(const CanvasShortcuts::Shortcut &s); CanvasShortcuts m_canvasShortcuts; - bool m_hasChanges; + QSet m_conflictRows; + QSet m_externalKeySequences; + bool m_hasChanges = false; }; #endif diff --git a/src/libclient/utils/customshortcutmodel.cpp b/src/libclient/utils/customshortcutmodel.cpp index 0c45e72b6..20e3b0fd8 100644 --- a/src/libclient/utils/customshortcutmodel.cpp +++ b/src/libclient/utils/customshortcutmodel.cpp @@ -32,8 +32,40 @@ QVariant CustomShortcutModel::data(const QModelIndex &index, int role) const return QVariant(); } - if(role == Qt::DisplayRole || role == Qt::EditRole) { + if(role == Qt::DisplayRole || role == Qt::ToolTipRole) { const CustomShortcut &cs = m_loadedShortcuts[m_shortcutIndexes[row]]; + QString text; + switch(Column(index.column())) { + case Action: + text = cs.title; + break; + case CurrentShortcut: + text = cs.currentShortcut.toString(QKeySequence::NativeText); + break; + case AlternateShortcut: + text = cs.alternateShortcut.toString(QKeySequence::NativeText); + break; + case DefaultShortcut: + text = cs.defaultShortcut.toString(QKeySequence::NativeText); + break; + default: + break; + } + if(role == Qt::ToolTipRole && m_conflictRows.contains(row)) { + if(text.isEmpty()) { + //: Tooltip for a keyboard shortcut conflict. + return tr("Conflict"); + } else { + //: Tooltip for a keyboard shortcut conflict, %1 is the name or + //: key sequence of the shortcut in question. + return tr("%1 (conflict)").arg(text); + } + } else { + return text; + } + } else if(role == Qt::EditRole) { + const CustomShortcut &cs = m_loadedShortcuts[m_shortcutIndexes[row]]; + QString text; switch(Column(index.column())) { case Action: return cs.title; @@ -46,18 +78,38 @@ QVariant CustomShortcutModel::data(const QModelIndex &index, int role) const default: return QVariant(); } - } else if(role == Qt::ToolTipRole) { - if(m_conflictRows.contains(row)) { - return tr("Conflict"); - } else { + } else if(role == Qt::DecorationRole) { + switch(index.column()) { + case int(Action): + return m_loadedShortcuts[m_shortcutIndexes[row]].icon; + case int(CurrentShortcut): { + QHash::const_iterator it = + m_conflictRows.constFind(row); + if(it != m_conflictRows.end() && it->current) { + return QIcon::fromTheme("dialog-warning"); + } else { + return QVariant(); + } + } + case int(AlternateShortcut): { + QHash::const_iterator it = + m_conflictRows.constFind(row); + if(it != m_conflictRows.end() && it->alternate) { + return QIcon::fromTheme("dialog-warning"); + } else { + return QVariant(); + } + } + default: return QVariant(); } - } else if(role == Qt::DecorationRole) { - if(index.column() == int(Action)) { - const CustomShortcut &cs = - m_loadedShortcuts[m_shortcutIndexes[row]]; - return cs.icon; - } else { + } else if(role == Qt::TextAlignmentRole) { + switch(index.column()) { + case int(CurrentShortcut): + case int(AlternateShortcut): + case int(DefaultShortcut): + return Qt::AlignCenter; + default: return QVariant(); } } else if(role == Qt::ForegroundRole) { @@ -70,8 +122,25 @@ QVariant CustomShortcutModel::data(const QModelIndex &index, int role) const if(m_conflictRows.contains(row)) { return QColor(0xdc3545); } else { - return QVariant{}; + return QVariant(); } + } else if(role == int(FilterRole)) { + const CustomShortcut &cs = m_loadedShortcuts[m_shortcutIndexes[row]]; + QString searchSuffix = cs.searchText.isEmpty() + ? QString() + : QStringLiteral(" (%1)").arg(cs.searchText); + QString conflictMarker = + m_conflictRows.contains(row) ? QStringLiteral("\n\1") : QString(); + return QStringLiteral( + "action:%1%2\nprimaryshortcut:%3\nalternateshortcut:%4\n" + "defaultprimaryshortcut:%5\ndefaultalternateshortcut:%6%7") + .arg( + cs.title, searchSuffix, + cs.currentShortcut.toString(QKeySequence::NativeText), + cs.alternateShortcut.toString(QKeySequence::NativeText), + cs.defaultShortcut.toString(QKeySequence::NativeText), + cs.defaultAlternateShortcut.toString(QKeySequence::NativeText), + conflictMarker); } else { return QVariant(); } @@ -95,7 +164,7 @@ bool CustomShortcutModel::setData( } emit dataChanged(index, index); - updateConflictRows(); + updateConflictRows(true); return true; } @@ -172,8 +241,8 @@ void CustomShortcutModel::loadShortcuts(const QVariantMap &cfg) m_loadedShortcuts.append(a); } - updateShortcuts(); - updateConflictRows(); + updateShortcutsInternal(); + updateConflictRows(false); endResetModel(); } @@ -201,10 +270,26 @@ void CustomShortcutModel::updateShortcuts() { beginResetModel(); updateShortcutsInternal(); - updateConflictRows(); + updateConflictRows(false); endResetModel(); } +const CustomShortcut &CustomShortcutModel::shortcutAt(int row) const +{ + Q_ASSERT(row >= 0); + Q_ASSERT(row < m_shortcutIndexes.size()); + return m_loadedShortcuts[m_shortcutIndexes[row]]; +} + +void CustomShortcutModel::setExternalKeySequences( + const QSet &externalKeySequences) +{ + if(m_externalKeySequences != externalKeySequences) { + m_externalKeySequences = externalKeySequences; + updateConflictRows(true); + } +} + bool CustomShortcutModel::isCustomizableActionRegistered(const QString &name) { return m_customizableActions.contains(name); @@ -213,14 +298,16 @@ bool CustomShortcutModel::isCustomizableActionRegistered(const QString &name) void CustomShortcutModel::registerCustomizableAction( const QString &name, const QString &title, const QIcon &icon, const QKeySequence &defaultShortcut, - const QKeySequence &defaultAlternateShortcut) + const QKeySequence &defaultAlternateShortcut, const QString &searchText) { - if(!m_customizableActions.contains(name)) { + if(m_customizableActions.contains(name)) { + qWarning( + "Attempt to re-register existing shortcut %s", + qUtf8Printable(name)); + } else { m_customizableActions.insert( - name, - CustomShortcut{ - name, title, icon, defaultShortcut, defaultAlternateShortcut, - QKeySequence(), QKeySequence()}); + name, {name, title, searchText, icon, defaultShortcut, + defaultAlternateShortcut, QKeySequence(), QKeySequence()}); } } @@ -275,35 +362,61 @@ void CustomShortcutModel::updateShortcutsInternal() } } -void CustomShortcutModel::updateConflictRows() +void CustomShortcutModel::updateConflictRows(bool emitDataChanges) { - QHash> rowsByKeySequence; + QHash> rowsByKeySequence; + for(const QKeySequence &ks : m_externalKeySequences) { + rowsByKeySequence[ks].insert(-1, {false, false}); + } + int rowCount = m_shortcutIndexes.size(); for(int row = 0; row < rowCount; ++row) { const CustomShortcut &shortcut = m_loadedShortcuts[m_shortcutIndexes[row]]; + if(!shortcut.currentShortcut.isEmpty()) { - rowsByKeySequence[shortcut.currentShortcut].insert(row); + rowsByKeySequence[shortcut.currentShortcut][row].current = true; } if(!shortcut.alternateShortcut.isEmpty()) { - rowsByKeySequence[shortcut.alternateShortcut].insert(row); + rowsByKeySequence[shortcut.alternateShortcut][row].alternate = true; } } - QSet conflictRows; - for(const QSet &values : rowsByKeySequence.values()) { + QHash conflictRows; + for(const QHash &values : rowsByKeySequence.values()) { if(values.size() > 1) { - conflictRows.unite(values); + for(QHash::const_iterator it = values.constBegin(), + end = values.constEnd(); + it != end; ++it) { + conflictRows[it.key()].mergeWith(it.value()); + } } } - for(int row : conflictRows + m_conflictRows) { - if(!conflictRows.contains(row) || !m_conflictRows.contains(row)) { - emit dataChanged( - createIndex(row, 0), createIndex(row, ColumnCount - 1), - {Qt::ForegroundRole, Qt::BackgroundRole}); + if(emitDataChanges) { + QSet keys; + for(const QHash &hash : {conflictRows, m_conflictRows}) { + for(QHash::key_iterator it = hash.keyBegin(), + end = hash.keyEnd(); + it != end; ++it) { + int row = *it; + if(row >= 0) { + keys.insert(row); + } + } + } + + for(int row : keys) { + QHash::const_iterator it1, it2; + if((it1 = conflictRows.constFind(row)) == conflictRows.constEnd() || + (it2 = m_conflictRows.constFind(row)) == + m_conflictRows.constEnd() || + it1.value() != it2.value()) { + emit dataChanged( + createIndex(row, 0), createIndex(row, ColumnCount - 1)); + } } } - m_conflictRows = conflictRows; + m_conflictRows.swap(conflictRows); } diff --git a/src/libclient/utils/customshortcutmodel.h b/src/libclient/utils/customshortcutmodel.h index 43bb8faff..d542bc9ef 100644 --- a/src/libclient/utils/customshortcutmodel.h +++ b/src/libclient/utils/customshortcutmodel.h @@ -2,6 +2,7 @@ #ifndef LIBCLIENT_UTILS_CUSTOMSHORTCUTMODEL_H #define LIBCLIENT_UTILS_CUSTOMSHORTCUTMODEL_H #include +#include #include #include #include @@ -11,6 +12,7 @@ struct CustomShortcut { QString name; QString title; + QString searchText; QIcon icon; QKeySequence defaultShortcut; QKeySequence defaultAlternateShortcut; @@ -34,6 +36,8 @@ class CustomShortcutModel final : public QAbstractTableModel { ColumnCount }; + enum Role { FilterRole = Qt::UserRole + 1 }; + explicit CustomShortcutModel(QObject *parent = nullptr); int rowCount(const QModelIndex &parent = QModelIndex()) const override; @@ -59,24 +63,54 @@ class CustomShortcutModel final : public QAbstractTableModel { void updateShortcuts(); + const CustomShortcut &shortcutAt(int row) const; + + bool hasConflicts() const { return !m_conflictRows.isEmpty(); } + + void + setExternalKeySequences(const QSet &externalKeySequences); + static QList getDefaultShortcuts(const QString &name); static bool isCustomizableActionRegistered(const QString &name); static void registerCustomizableAction( const QString &name, const QString &title, const QIcon &icon, const QKeySequence &defaultShortcut, - const QKeySequence &defaultAlternateShortcut); + const QKeySequence &defaultAlternateShortcut, + const QString &searchText = QString()); static void setCustomizableActionIcon(const QString &name, const QIcon &icon); static void changeDisabledActionNames( const QVector> &nameDisabledPairs); private: + struct Conflict { + bool current = false; + bool alternate = false; + + bool operator==(Conflict &other) const + { + return current == other.current && alternate == other.alternate; + } + + bool operator!=(const Conflict &other) const + { + return current != other.current || alternate != other.alternate; + } + + void mergeWith(const Conflict &other) + { + current = current || other.current; + alternate = alternate || other.alternate; + } + }; + void updateShortcutsInternal(); - void updateConflictRows(); + void updateConflictRows(bool emitDataChanges); QVector m_shortcutIndexes; QVector m_loadedShortcuts; - QSet m_conflictRows; + QHash m_conflictRows; + QSet m_externalKeySequences; static QMap m_customizableActions; static QSet m_disabledActionNames;