12.2b: the Keyboard Shortcuts window and remapped keys everywhere (Compositor 1.1.6)

This commit is contained in:
雷电芽衣 committed 2026-09-26 19:17:50 -04:00
1 parent f1b2c65c1e
commit 4bfc6c237e
29 files changed
+1645 -164

No files matched your search

+2 -2
View File
@@ -31,7 +31,7 @@ void CompositorMenusTests::everyEntryHasSwiftsShortcutWithCtrlForCommand()
{"exportPNG", "Ctrl+Shift+E"}, {"exportJPEG", "Ctrl+Alt+Shift+S"},
{"closeProject", "Ctrl+W"}, {"undo", "Ctrl+Z"}, {"redo", "Ctrl+Shift+Z"}, {"fit", "Ctrl+0"}, {"actualPixels", "Ctrl+1"},
{"zoomIn", "Ctrl+="}, {"zoomOut", "Ctrl+-"}, {"pixelGrid", ""}, {"snap", ""}, {"transformControls", "Ctrl+H"}, {"transformLayer", "Ctrl+T"},
{"layerViaCopy", "Ctrl+J"}, {"cut", "Ctrl+X"}, {"copy", "Ctrl+C"}, {"copyMerged", "Ctrl+Shift+C"}, {"paste", "Ctrl+V"}, {"fillForeground", "Alt+Backspace"}, {"fillBackground", "Ctrl+Backspace"}, {"clearSelectionPixels", ""}, {"contentAwareFill", "Shift+Backspace"}, {"clippingMask", "Ctrl+Alt+G"}, {"groupLayers", "Ctrl+G"},
{"layerViaCopy", "Ctrl+J"}, {"cut", "Ctrl+X"}, {"copy", "Ctrl+C"}, {"copyMerged", "Ctrl+Shift+C"}, {"paste", "Ctrl+V"}, {"keyboardShortcuts", ""}, {"fillForeground", "Alt+Backspace"}, {"fillBackground", "Ctrl+Backspace"}, {"clearSelectionPixels", ""}, {"contentAwareFill", "Shift+Backspace"}, {"clippingMask", "Ctrl+Alt+G"}, {"groupLayers", "Ctrl+G"},
{"moveOutOfFolder", ""}, {"newBlankLayer", "Ctrl+Shift+N"}, {"renameLayer", ""}, {"layerVisibility", ""}, {"moveLayerUp", "Ctrl+]"}, {"moveLayerDown", "Ctrl+["},
{"mergeLayers", "Ctrl+E"}, {"flipHorizontal", ""}, {"flipVertical", ""}, {"deleteLayer", ""},
{"selectAll", "Ctrl+A"}, {"deselect", "Ctrl+D"}, {"inverse", "Ctrl+Shift+I"}, {"layerPixels", ""}, {"subject", "Ctrl+Alt+A"}, {"maskBlackAreas", ""},
@@ -50,7 +50,7 @@ void CompositorMenusTests::everyEntryHasSwiftsShortcutWithCtrlForCommand()
titles << menu->title();
QCOMPARE(titles, (QStringList{"&File", "&Edit", "&View", "&Select", "&Image", "Fil&ter", "&Layer"}));
QCOMPARE(menus[0]->actions().size(), 11);
QCOMPARE(menus[1]->actions().size(), 12);
QCOMPARE(menus[1]->actions().size(), 13);
QCOMPARE(menus[2]->actions().size(), 7);
QCOMPARE(menus[3]->actions().size(), 10);
QCOMPARE(menus[4]->actions().size(), 13);
+186
View File
@@ -0,0 +1,186 @@
#include "MenuFixtures.h"
#include "UI/KeyboardShortcuts.h"
#include <QApplication>
#include <QLabel>
#include <QSettings>
#include <QStandardPaths>
// The Keyboard Shortcuts window and the menus it remaps.
namespace {
QWidget *shownPanel()
{
for (QWidget *widget : QApplication::topLevelWidgets()) {
if (widget->objectName() == QStringLiteral("keyboardShortcuts") && widget->isVisible())
return widget;
}
return nullptr;
}
template <typename Widget> Widget &find(QWidget &root, const QString &name)
{
Widget *found = root.findChild<Widget *>(name);
if (!found)
throw std::runtime_error("no widget named " + name.toStdString());
return *found;
}
QWidget &opened(Bar &bar)
{
// A shown window's menus hold live shortcuts.
bar.window.show();
if (!QTest::qWaitForWindowExposed(&bar.window))
throw std::runtime_error("the window never showed");
bar.action("keyboardShortcuts").trigger();
QWidget *panel = nullptr;
if (!QTest::qWaitFor([&] { return (panel = shownPanel()) != nullptr; }))
throw std::runtime_error("the panel never showed");
return *panel;
}
}
class KeyboardShortcutsSheetTests : public QObject {
Q_OBJECT
private slots:
void initTestCase();
void cleanup();
void aRecordedKeyRemapsItsMenuEntryOnSave();
void recordingWaitsForARealKey();
void aClashRestsSaveAndDefaultsComeBack();
void cancelKeepsWhatWasSaved();
};
void KeyboardShortcutsSheetTests::initTestCase()
{
QStandardPaths::setTestModeEnabled(true);
QSettings().remove(QLatin1String(ShortcutSettings::storageKey));
ShortcutSettings::shared().reload();
}
void KeyboardShortcutsSheetTests::cleanup()
{
QSettings().remove(QLatin1String(ShortcutSettings::storageKey));
ShortcutSettings::shared().reload();
if (QWidget *panel = shownPanel())
panel->hide();
}
void KeyboardShortcutsSheetTests::aRecordedKeyRemapsItsMenuEntryOnSave()
{
Bar bar;
QCOMPARE(bar.action("keyboardShortcuts").text(), QString("Keyboard Shortcuts…"));
QWidget &panel = opened(bar);
QCOMPARE(panel.windowTitle(), QString("Keyboard Shortcuts"));
auto &sheet = find<QWidget>(panel, "keyboardShortcutsSheet");
QCOMPARE(sheet.width(), 660);
auto &undo = find<ShortcutRecorder>(sheet, "recorder:Menus:Undo");
QCOMPARE(undo.text(), QString("Ctrl+Z"));
QCOMPARE(undo.size(), QSize(150, 26));
QTRY_COMPARE(QApplication::activeWindow(), &panel);
undo.click();
QVERIFY(undo.text() == "Press keys…" && undo.accessibleName() == "Press a shortcut");
QTRY_VERIFY(undo.hasFocus());
// Save rests while a key is being recorded.
QVERIFY(!find<QPushButton>(sheet, "saveShortcuts").isEnabled());
QTest::keyClick(&undo, Qt::Key_K, Qt::ControlModifier | Qt::AltModifier);
QCOMPARE(undo.text(), QString("Ctrl+Alt+K"));
// Recording done, the button lets go of the keys.
QVERIFY(!undo.hasFocus());
// Nothing applies until Save.
QCOMPARE(bar.action("undo").shortcut(), QKeySequence(Qt::CTRL | Qt::Key_Z));
find<QPushButton>(sheet, "saveShortcuts").click();
QTRY_VERIFY(!shownPanel());
QCOMPARE(bar.action("undo").shortcut(), QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_K));
QCOMPARE(ShortcutSettings::shared().overrides().value("Menus:Undo"), ShortcutChord("k", 3));
// An entry Swift does not list keeps its key.
QCOMPARE(bar.action("fit").shortcut(), QKeySequence(Qt::CTRL | Qt::Key_0));
// A new window reads the saved keys too.
Bar again;
QCOMPARE(again.action("undo").shortcut(), QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_K));
ShortcutSettings::shared().save({});
QCOMPARE(bar.action("undo").shortcut(), QKeySequence(Qt::CTRL | Qt::Key_Z));
}
void KeyboardShortcutsSheetTests::recordingWaitsForARealKey()
{
Bar bar;
bar.session().createDocument(20, 20);
QWidget &panel = opened(bar);
auto &brush = find<ShortcutRecorder>(panel, "recorder:Canvas & Layers:Brush tool");
QCOMPARE(brush.text(), QString("B"));
brush.click();
// A modifier alone waits; a long key name is refused.
QTest::keyClick(&brush, Qt::Key_Control, Qt::ControlModifier);
QTest::keyClick(&brush, Qt::Key_F1);
QCOMPARE(brush.text(), QString("Press keys…"));
// A menu's own key is recorded, not run.
const size_t layers = bar.session().document().value().layers.size();
QVERIFY(bar.action("newBlankLayer").isEnabled());
QTest::keyClick(&brush, Qt::Key_N, Qt::ControlModifier | Qt::ShiftModifier);
QCOMPARE(bar.session().document().value().layers.size(), layers);
QCOMPARE(brush.text(), QString("Ctrl+Shift+N"));
// The clash with New Blank Layer rests Save.
QVERIFY(!find<QPushButton>(panel, "saveShortcuts").isEnabled());
QVERIFY(find<QLabel>(panel, "shortcutProblem").isVisible());
// Tab is recorded too, never moving the focus.
brush.click();
QTRY_VERIFY(brush.hasFocus());
QTest::keyClick(&brush, Qt::Key_Tab);
QCOMPARE(brush.text(), QString("Tab"));
}
void KeyboardShortcutsSheetTests::aClashRestsSaveAndDefaultsComeBack()
{
Bar bar;
QWidget &panel = opened(bar);
auto &brush = find<ShortcutRecorder>(panel, "recorder:Canvas & Layers:Brush tool");
auto &problem = find<QLabel>(panel, "shortcutProblem");
auto &save = find<QPushButton>(panel, "saveShortcuts");
QVERIFY(!problem.isVisible() && save.isEnabled() && save.isDefault());
brush.click();
QTest::keyClick(&brush, Qt::Key_V);
QCOMPARE(problem.text(), QString("V is assigned to both Move / Transform tool and Brush tool."));
QVERIFY(problem.isVisible() && !save.isEnabled());
QCOMPARE(problem.font().pixelSize(), 12);
// Search hides rows by title, whatever the case.
auto &search = find<QLineEdit>(panel, "shortcutSearch");
QCOMPARE(search.placeholderText(), QString("Search shortcuts"));
search.setText("BRUSH");
QVERIFY(brush.parentWidget()->isVisible());
QVERIFY(!find<ShortcutRecorder>(panel, "recorder:Menus:Undo").parentWidget()->isVisible());
search.clear();
QVERIFY(find<ShortcutRecorder>(panel, "recorder:Menus:Undo").parentWidget()->isVisible());
// Restore Defaults also ends a recording.
brush.click();
QCOMPARE(brush.text(), QString("Press keys…"));
find<QPushButton>(panel, "restoreShortcuts").click();
QVERIFY(!problem.isVisible() && save.isEnabled() && brush.text() == "B");
// Return saves, Save being the default.
QTRY_COMPARE(QApplication::activeWindow(), &panel);
QTest::keyClick(&search, Qt::Key_Return);
QTRY_VERIFY(!shownPanel());
QVERIFY(ShortcutSettings::shared().overrides().isEmpty());
}
void KeyboardShortcutsSheetTests::cancelKeepsWhatWasSaved()
{
Bar bar;
ShortcutSettings::shared().save({{"Canvas & Layers:Brush tool", ShortcutChord("k")}});
QWidget &panel = opened(bar);
auto &brush = find<ShortcutRecorder>(panel, "recorder:Canvas & Layers:Brush tool");
// The sheet starts from what is saved.
QCOMPARE(brush.text(), QString("K"));
brush.click();
QTest::keyClick(&brush, Qt::Key_Y);
find<QPushButton>(panel, "cancelShortcuts").click();
QTRY_VERIFY(!shownPanel());
QCOMPARE(ShortcutSettings::shared().overrides().value("Canvas & Layers:Brush tool"), ShortcutChord("k"));
// Escape closes it as Cancel does.
QWidget &again = opened(bar);
QTRY_COMPARE(QApplication::activeWindow(), &again);
QTest::keyClick(QApplication::focusWidget() ? QApplication::focusWidget() : &again, Qt::Key_Escape);
QTRY_VERIFY(!shownPanel());
QCOMPARE(ShortcutSettings::shared().overrides().size(), 1);
}
QTEST_MAIN(KeyboardShortcutsSheetTests)
#include "KeyboardShortcutsSheetTests.moc"
+223
View File
@@ -0,0 +1,223 @@
#include "UI/KeyboardShortcuts.h"
#include <QKeyEvent>
#include <QSet>
#include <QSettings>
#include <QSignalSpy>
#include <QStandardPaths>
#include <QtTest>
// Swift's shortcut model: chords, definitions, rules, rewriting.
namespace {
QKeyEvent press(Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier, const QString &text = QString())
{
return QKeyEvent(QEvent::KeyPress, key, modifiers, text);
}
const ShortcutDefinition &named(const QString &title)
{
for (const ShortcutDefinition &definition : ShortcutDefinition::all()) {
if (definition.title == title)
return definition;
}
throw std::runtime_error("no shortcut named " + title.toStdString());
}
void clear()
{
QSettings().remove(QLatin1String(ShortcutSettings::storageKey));
ShortcutSettings::shared().reload();
}
}
class KeyboardShortcutsTests : public QObject {
Q_OBJECT
private slots:
void initTestCase();
void cleanup();
void chordsReadKeysAsSwiftDoes();
void theListIsSwiftsWithoutHide();
void problemsNameWhatIsWrong();
void savedOverridesPersistAndBadOnesAreIgnored();
void menusAndSheetsTakeTheirRemappedKeys();
void theCanvasTranslatesRemappedKeys();
void openTextTranslatesItsOwnKeys();
};
void KeyboardShortcutsTests::initTestCase()
{
QStandardPaths::setTestModeEnabled(true);
clear();
}
void KeyboardShortcutsTests::cleanup()
{
clear();
}
void KeyboardShortcutsTests::chordsReadKeysAsSwiftDoes()
{
QCOMPARE(ShortcutChord(press(Qt::Key_B)), ShortcutChord("b"));
QCOMPARE(ShortcutChord(press(Qt::Key_Z, Qt::ControlModifier | Qt::ShiftModifier)), ShortcutChord("z", 9));
QCOMPARE(ShortcutChord(press(Qt::Key_H, Qt::AltModifier | Qt::MetaModifier)), ShortcutChord("h", 6));
// Shifted brackets and signs are their own keys.
QCOMPARE(ShortcutChord(press(Qt::Key_BraceLeft, Qt::ShiftModifier)), ShortcutChord("[", 8));
QCOMPARE(ShortcutChord(press(Qt::Key_BraceRight, Qt::ShiftModifier)), ShortcutChord("]", 8));
QCOMPARE(ShortcutChord(press(Qt::Key_Plus, Qt::ShiftModifier)), ShortcutChord("=", 8));
QCOMPARE(ShortcutChord(press(Qt::Key_Underscore, Qt::ShiftModifier)), ShortcutChord("-", 8));
// Both deletes, returns and tabs are one key each.
QCOMPARE(ShortcutChord(press(Qt::Key_Backspace)), ShortcutChord("\x7f"));
QCOMPARE(ShortcutChord(press(Qt::Key_Delete)), ShortcutChord("\x7f"));
QCOMPARE(ShortcutChord(press(Qt::Key_Enter)), ShortcutChord("\r"));
QCOMPARE(ShortcutChord(press(Qt::Key_Backtab, Qt::ShiftModifier)), ShortcutChord("\t", 8));
QCOMPARE(ShortcutChord(press(Qt::Key_Escape)), ShortcutChord("\x1b"));
QCOMPARE(ShortcutChord(press(Qt::Key_Left)), ShortcutChord(QString(QChar(0xf702))));
QCOMPARE(ShortcutChord(press(Qt::Key_Down)).key, QString(QChar(0xf701)));
QCOMPARE(ShortcutChord(press(Qt::Key_F1)).key.size(), 2);
// Labels and key combinations, as the menus show them.
QCOMPARE(ShortcutChord("s", 11).label(), QString("Ctrl+Alt+Shift+S"));
QCOMPARE(ShortcutChord("\x7f", 2).label(), QString("Alt+Backspace"));
QCOMPARE(ShortcutChord("[", 8).combination(), QKeyCombination(Qt::ShiftModifier, Qt::Key_BraceLeft));
QCOMPARE(ShortcutChord(QString(QChar(0xf700)), 1).combination(), QKeyCombination(Qt::ControlModifier, Qt::Key_Up));
QCOMPARE(ShortcutChord(QKeyCombination(Qt::ControlModifier, Qt::Key_Equal)), ShortcutChord("=", 1));
// A chord typed again carries its key, text and repeat.
const QKeyEvent like(QEvent::KeyPress, Qt::Key_K, Qt::NoModifier, QStringLiteral("k"), true);
const std::unique_ptr<QKeyEvent> typed = ShortcutChord("b").event(like);
QVERIFY(typed->key() == Qt::Key_B && typed->modifiers() == Qt::NoModifier && typed->text() == "b" && typed->isAutoRepeat());
const std::unique_ptr<QKeyEvent> space = ShortcutChord(" ").event(like);
QVERIFY(space->key() == Qt::Key_Space && space->text().isEmpty());
const std::unique_ptr<QKeyEvent> hard = ShortcutChord("]", 8).event(like);
QVERIFY(hard->key() == Qt::Key_BraceRight && hard->modifiers() == Qt::ShiftModifier && hard->text() == "}");
}
void KeyboardShortcutsTests::theListIsSwiftsWithoutHide()
{
const std::vector<ShortcutDefinition> &all = ShortcutDefinition::all();
// Swift's 107, less Hide Compositor.
QCOMPARE(int(all.size()), 106);
QSet<QString> ids;
for (const ShortcutDefinition &definition : all)
ids.insert(definition.id());
QCOMPARE(int(ids.size()), int(all.size()));
QVERIFY(!ids.contains("Menus:Hide Compositor"));
QCOMPARE(named("Export JPEG").original, ShortcutChord("s", 11));
QVERIFY(named("Export JPEG").isMenu());
QCOMPARE(named("Content-Aware Fill").original, ShortcutChord("\x7f", 8));
QCOMPARE(named("Temporary Hand tool (hold)").original, ShortcutChord(" "));
QCOMPARE(named("Opacity digit 7 (type two for exact %)").original, ShortcutChord("7"));
QCOMPARE(named("Move selected pixels Down 10 px").original, ShortcutChord(QString(QChar(0xf701)), 9));
QCOMPARE(named("Increase leading by 10").group, QString("Text Editing"));
QCOMPARE(named("Increase leading by 10").original, ShortcutChord(QString(QChar(0xf701)), 10));
QCOMPARE(named("Toggle Levels preview").group, QString("Canvas & Layers"));
QCOMPARE(named("Undo").id(), QString("Menus:Undo"));
// Swift's defaults hold together.
QVERIFY(!ShortcutSettings::problem({}));
}
void KeyboardShortcutsTests::problemsNameWhatIsWrong()
{
const QString brush = named("Brush tool").id(), finish = named("Finish editing text").id();
QCOMPARE(ShortcutSettings::problem({{brush, ShortcutChord("f1")}}).value(), QString("Choose a single key with optional modifiers."));
QCOMPARE(ShortcutSettings::problem({{brush, ShortcutChord("k", 16)}}).value(), QString("Choose a single key with optional modifiers."));
QCOMPARE(ShortcutSettings::problem({{brush, ShortcutChord("k", -1)}}).value(), QString("Choose a single key with optional modifiers."));
QCOMPARE(ShortcutSettings::problem({{finish, ShortcutChord("\r", 8)}}).value(),
QString("Text-editing shortcuts need Ctrl, Alt or Meta so they do not replace normal typing."));
QVERIFY(!ShortcutSettings::problem({{finish, ShortcutChord("\r", 4)}}));
QCOMPARE(ShortcutSettings::problem({{brush, ShortcutChord("v")}}).value(), QString("V is assigned to both Move / Transform tool and Brush tool."));
// Moving both apart is fine.
QVERIFY(!ShortcutSettings::problem({{brush, ShortcutChord("v")}, {named("Move / Transform tool").id(), ShortcutChord("b")}}));
}
void KeyboardShortcutsTests::savedOverridesPersistAndBadOnesAreIgnored()
{
ShortcutSettings &settings = ShortcutSettings::shared();
QSignalSpy changes(&settings, &ShortcutSettings::changed);
const QString brush = named("Brush tool").id();
QVERIFY(!settings.save({{brush, ShortcutChord("v")}}));
QVERIFY(settings.overrides().isEmpty() && changes.isEmpty());
QVERIFY(settings.save({{brush, ShortcutChord("k")}}));
QCOMPARE(changes.count(), 1);
QCOMPARE(settings.chord(named("Brush tool")), ShortcutChord("k"));
QCOMPARE(settings.chord(named("Eraser")), ShortcutChord("e"));
// Read again as at launch, from QSettings.
settings.reload();
QCOMPARE(settings.overrides(), (QHash<QString, ShortcutChord>{{brush, ShortcutChord("k")}}));
QCOMPARE(QSettings().value(QLatin1String(ShortcutSettings::storageKey)).toByteArray(),
QByteArray(R"({"Canvas & Layers:Brush tool":{"key":"k","modifiers":0}})"));
// Stored overrides that clash are dropped with a warning.
QSettings().setValue(QLatin1String(ShortcutSettings::storageKey), QByteArray(R"({"Canvas & Layers:Brush tool":{"key":"v","modifiers":0}})"));
QTest::ignoreMessage(QtWarningMsg, "stored keyboard shortcuts ignored: V is assigned to both Move / Transform tool and Brush tool.");
settings.reload();
QVERIFY(settings.overrides().isEmpty());
}
void KeyboardShortcutsTests::menusAndSheetsTakeTheirRemappedKeys()
{
ShortcutSettings &settings = ShortcutSettings::shared();
const QKeySequence undo(Qt::CTRL | Qt::Key_Z);
QCOMPARE(settings.menu(undo), undo);
QVERIFY(settings.save({{named("Undo").id(), ShortcutChord("u", 3)}, {named("Hue/Saturation").id(), ShortcutChord("h", 3)},
{named("Apply current canvas operation").id(), ShortcutChord("k")}}));
QCOMPARE(settings.menu(undo), QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_U));
// An unlisted entry keeps its key; none stays none.
QCOMPARE(settings.menu(QKeySequence(Qt::CTRL | Qt::Key_L)), QKeySequence(Qt::CTRL | Qt::Key_L));
QCOMPARE(settings.menu(QKeySequence(Qt::CTRL | Qt::Key_H)), QKeySequence(Qt::CTRL | Qt::Key_H));
QCOMPARE(settings.menu(QKeySequence()), QKeySequence());
// Sheets read the canvas's Apply and Cancel.
QCOMPARE(settings.native(ShortcutChord("\r")), ShortcutChord("k"));
QCOMPARE(settings.native(ShortcutChord("\x1b")), ShortcutChord("\x1b"));
QCOMPARE(settings.native(ShortcutChord("y", 1)), ShortcutChord("y", 1));
// A menu's chord is no sheet's.
QCOMPARE(settings.native(ShortcutChord("z", 1)), ShortcutChord("z", 1));
}
void KeyboardShortcutsTests::theCanvasTranslatesRemappedKeys()
{
ShortcutSettings &settings = ShortcutSettings::shared();
// Without overrides every key passes as it is.
std::unique_ptr<QKeyEvent> same = settings.canvasEvent(press(Qt::Key_B, Qt::NoModifier, "b"));
QVERIFY(same && same->key() == Qt::Key_B);
QVERIFY(settings.save({{named("Brush tool").id(), ShortcutChord("k")}, {named("Undo").id(), ShortcutChord("u", 3)},
{named("Cycle shape kind").id(), ShortcutChord("y", 8)}}));
// The new key stands for the old; the old goes.
const std::unique_ptr<QKeyEvent> brush = settings.canvasEvent(press(Qt::Key_K, Qt::NoModifier, "k"));
QVERIFY(brush && brush->key() == Qt::Key_B && brush->modifiers() == Qt::NoModifier);
QVERIFY(!settings.canvasEvent(press(Qt::Key_B, Qt::NoModifier, "b")));
// A menu's old chord is swallowed at the canvas too.
QVERIFY(!settings.canvasEvent(press(Qt::Key_Z, Qt::ControlModifier)));
// Shift follows a letter home, unless Shift has its own.
const std::unique_ptr<QKeyEvent> shifted = settings.canvasEvent(press(Qt::Key_K, Qt::ShiftModifier, "K"));
QVERIFY(shifted && shifted->key() == Qt::Key_B && shifted->modifiers() == Qt::ShiftModifier);
QVERIFY(!settings.canvasEvent(press(Qt::Key_B, Qt::ShiftModifier, "B")));
const std::unique_ptr<QKeyEvent> kind = settings.canvasEvent(press(Qt::Key_Y, Qt::ShiftModifier, "Y"));
QVERIFY(kind && kind->key() == Qt::Key_U && kind->modifiers() == Qt::ShiftModifier);
QVERIFY(!settings.canvasEvent(press(Qt::Key_U, Qt::ShiftModifier, "U")));
// Keys nobody moved pass untouched.
const std::unique_ptr<QKeyEvent> eraser = settings.canvasEvent(press(Qt::Key_E, Qt::ShiftModifier, "E"));
QVERIFY(eraser && eraser->key() == Qt::Key_E && eraser->modifiers() == Qt::ShiftModifier);
const std::unique_ptr<QKeyEvent> other = settings.canvasEvent(press(Qt::Key_Q, Qt::NoModifier, "q"));
QVERIFY(other && other->key() == Qt::Key_Q);
}
void KeyboardShortcutsTests::openTextTranslatesItsOwnKeys()
{
ShortcutSettings &settings = ShortcutSettings::shared();
QVERIFY(settings.save({{named("Finish editing text").id(), ShortcutChord("\r", 4)}, {named("Brush tool").id(), ShortcutChord("k")},
{named("Cancel current canvas operation").id(), ShortcutChord("q", 1)}}));
const std::unique_ptr<QKeyEvent> finish = settings.textEvent(press(Qt::Key_Return, Qt::MetaModifier));
QVERIFY(finish && finish->key() == Qt::Key_Return && finish->modifiers() == Qt::ControlModifier);
QVERIFY(!settings.textEvent(press(Qt::Key_Return, Qt::ControlModifier)));
// Escape follows Cancel; tool letters stay letters.
const std::unique_ptr<QKeyEvent> cancel = settings.textEvent(press(Qt::Key_Q, Qt::ControlModifier));
QVERIFY(cancel && cancel->key() == Qt::Key_Escape && cancel->modifiers() == Qt::NoModifier);
QVERIFY(!settings.textEvent(press(Qt::Key_Escape)));
const std::unique_ptr<QKeyEvent> letter = settings.textEvent(press(Qt::Key_K, Qt::NoModifier, "k"));
QVERIFY(letter && letter->key() == Qt::Key_K && letter->text() == "k");
const std::unique_ptr<QKeyEvent> b = settings.textEvent(press(Qt::Key_B, Qt::NoModifier, "b"));
QVERIFY(b && b->key() == Qt::Key_B);
// The canvas leaves a text chord's old key alone.
const std::unique_ptr<QKeyEvent> canvas = settings.canvasEvent(press(Qt::Key_Return, Qt::ControlModifier));
QVERIFY(canvas && canvas->key() == Qt::Key_Return && canvas->modifiers() == Qt::ControlModifier);
}
QTEST_MAIN(KeyboardShortcutsTests)
#include "KeyboardShortcutsTests.moc"
+272
View File
@@ -0,0 +1,272 @@
#include "InlineTextFixtures.h"
#include "UI/CanvasSizeSheet.h"
#include "UI/ColorPickerSheet.h"
#include "UI/EffectsSheet.h"
#include "UI/FilterSheet.h"
#include "UI/HueSaturationSheet.h"
#include "UI/ImageSizeSheet.h"
#include "UI/JPEGExportSheet.h"
#include "UI/KeyboardShortcuts.h"
#include "UI/LevelsSheet.h"
#include "UI/NewCanvasSheet.h"
#include "UI/TransformInspector.h"
#include <QShortcut>
#include "UI/LassoControls.h"
#include "UI/NativeLayerList.h"
#include <QDialog>
#include <QSettings>
#include <QStandardPaths>
#include <QVBoxLayout>
// Remapped keys on canvas, list, sheets and open text.
namespace {
void remap(const QHash<QString, ShortcutChord> &values)
{
if (!ShortcutSettings::shared().save(values))
throw std::runtime_error("the overrides were refused");
}
}
class RemappedKeysTests : public QObject {
Q_OBJECT
private slots:
void initTestCase();
void cleanup();
void theCanvasReadsTheNewKeysAndDropsTheOld();
void aRemappedSpacePansUntilItsOwnKeyIsLetGo();
void theListAndTheWindowReadTheNewKeys();
void aSheetsButtonsFollowApplyAndCancel();
void openTextReadsItsOwnNewKeys();
void everySheetBindsSwiftsKeys();
void aKeyLeftAloneReachesTheParent();
};
namespace {
// Counts the keys its children leave to it.
struct Parent : QWidget {
int keys = 0;
void keyPressEvent(QKeyEvent *event) override
{
keys += 1;
event->accept();
}
};
}
void RemappedKeysTests::initTestCase()
{
QStandardPaths::setTestModeEnabled(true);
}
void RemappedKeysTests::cleanup()
{
QSettings().remove(QLatin1String(ShortcutSettings::storageKey));
ShortcutSettings::shared().reload();
}
void RemappedKeysTests::theCanvasReadsTheNewKeysAndDropsTheOld()
{
Canvas shown;
remap({{"Canvas & Layers:Brush tool", ShortcutChord("k")}, {"Canvas & Layers:Increase brush size", ShortcutChord("o")}});
QTRY_VERIFY(shown.canvas->hasFocus());
QTest::keyClick(shown.canvas, Qt::Key_B);
QCOMPARE(shown.session.tool(), NavigationTool::marquee);
QTest::keyClick(shown.canvas, Qt::Key_K);
QCOMPARE(shown.session.tool(), NavigationTool::brush);
const double size = shown.session.brushSettings().diameter;
QTest::keyClick(shown.canvas, Qt::Key_O);
QVERIFY(shown.session.brushSettings().diameter > size);
const double larger = shown.session.brushSettings().diameter;
QTest::keyClick(shown.canvas, Qt::Key_BracketRight);
QCOMPARE(shown.session.brushSettings().diameter, larger);
// A remapped hardness key steps hardness, not the size.
remap({{"Canvas & Layers:Brush tool", ShortcutChord("k")}, {"Canvas & Layers:Decrease brush hardness", ShortcutChord("o", 8)}});
const double hardness = shown.session.brushSettings().hardness;
QTest::keyClick(shown.canvas, Qt::Key_O, Qt::ShiftModifier);
QVERIFY(shown.session.brushSettings().hardness < hardness);
QCOMPARE(shown.session.brushSettings().diameter, larger);
}
void RemappedKeysTests::aRemappedSpacePansUntilItsOwnKeyIsLetGo()
{
Canvas shown;
remap({{"Canvas & Layers:Temporary Hand tool (hold)", ShortcutChord("q")}});
QTRY_VERIFY(shown.canvas->hasFocus());
const Qt::CursorShape resting = shown.canvas->cursor().shape();
QTest::keyPress(shown.canvas, Qt::Key_Q);
QCOMPARE(shown.canvas->cursor().shape(), Qt::OpenHandCursor);
// Space itself is swallowed; Q's release ends the pan.
QTest::keyRelease(shown.canvas, Qt::Key_Space);
QCOMPARE(shown.canvas->cursor().shape(), Qt::OpenHandCursor);
QTest::keyRelease(shown.canvas, Qt::Key_Q);
QCOMPARE(shown.canvas->cursor().shape(), resting);
QTest::keyPress(shown.canvas, Qt::Key_Space);
QCOMPARE(shown.canvas->cursor().shape(), resting);
// Unmapped again, Space pans and ends as before.
cleanup();
QTest::keyPress(shown.canvas, Qt::Key_Space);
QCOMPARE(shown.canvas->cursor().shape(), Qt::OpenHandCursor);
QTest::keyRelease(shown.canvas, Qt::Key_Space);
QCOMPARE(shown.canvas->cursor().shape(), resting);
}
void RemappedKeysTests::theListAndTheWindowReadTheNewKeys()
{
EditorSession session;
session.createDocument(100, 100, true);
QWidget window;
auto *layout = new QVBoxLayout(&window);
auto *list = new NativeLayerList(session, &window);
auto *field = new QLineEdit(&window);
layout->addWidget(list);
layout->addWidget(field);
CanvasView canvas(session, &window);
window.resize(300, 500);
window.show();
QVERIFY(QTest::qWaitForWindowActive(&window));
remap({{"Canvas & Layers:Crop tool", ShortcutChord("k")}, {"Canvas & Layers:Next blend mode", ShortcutChord("n", 8)}});
list->setFocus();
QTRY_VERIFY(list->hasFocus());
QTest::keyClick(list, Qt::Key_C);
QVERIFY(session.tool() != NavigationTool::crop);
QTest::keyClick(list, Qt::Key_K);
QCOMPARE(session.tool(), NavigationTool::crop);
// The window blend keys read the new chord anywhere.
session.selectTool(NavigationTool::move);
session.insert(ImportedImage(QImage(4, 4, QImage::Format_RGBA8888_Premultiplied), QImage(), "Pixels"));
QTest::keyClick(list, Qt::Key_Plus, Qt::ShiftModifier);
QCOMPARE(session.activeLayer().value().blendMode, LayerBlendMode::normal);
QTest::keyClick(list, Qt::Key_N, Qt::ShiftModifier);
QCOMPARE(session.activeLayer().value().blendMode, allLayerBlendModes[1]);
}
void RemappedKeysTests::aSheetsButtonsFollowApplyAndCancel()
{
const auto session = selectionSession();
lasso(*session, square(20, 20, 40));
session->promptSelectionAmount(SelectionAmountOperation::feather);
remap({{"Canvas & Layers:Apply current canvas operation", ShortcutChord("k", 1)}});
QDialog dialog;
auto *sheet = new SelectionAmountSheet(*session, SelectionAmountOperation::feather, &dialog);
(new QVBoxLayout(&dialog))->addWidget(sheet);
dialog.show();
QVERIFY(QTest::qWaitForWindowActive(&dialog));
auto &field = *sheet->findChild<QLineEdit *>("amountField");
QTRY_VERIFY(field.hasFocus());
// Return no longer takes OK; the new chord does.
QTest::keyClick(&field, Qt::Key_Return);
QVERIFY(session->selectionAmountOperation().has_value());
// A resting OK takes no key either.
field.setText(QStringLiteral("0"));
QTest::keyClick(&field, Qt::Key_K, Qt::ControlModifier);
QVERIFY(session->selectionAmountOperation().has_value());
field.setText(QStringLiteral("2"));
auto &ok = *sheet->findChild<QPushButton *>("amountOK");
ok.hide();
QTest::keyClick(&field, Qt::Key_K, Qt::ControlModifier);
QVERIFY(session->selectionAmountOperation().has_value());
ok.show();
QTest::keyClick(&field, Qt::Key_K, Qt::ControlModifier);
QVERIFY(!session->selectionAmountOperation());
QCOMPARE(session->selection().value().feather, 2.0);
// Back to Return, the default button takes it again.
cleanup();
session->promptSelectionAmount(SelectionAmountOperation::feather);
QTest::keyClick(&field, Qt::Key_K, Qt::ControlModifier);
QVERIFY(session->selectionAmountOperation().has_value());
QTest::keyClick(&field, Qt::Key_Return);
QVERIFY(!session->selectionAmountOperation());
// Cancel moved onto Return takes it; Apply is elsewhere.
remap({{"Canvas & Layers:Apply current canvas operation", ShortcutChord("k", 1)},
{"Canvas & Layers:Cancel current canvas operation", ShortcutChord("\r")}});
session->promptSelectionAmount(SelectionAmountOperation::feather);
const double feather = session->selection().value().feather;
QTest::keyClick(&field, Qt::Key_Return);
QVERIFY(!session->selectionAmountOperation());
QCOMPARE(session->selection().value().feather, feather);
}
void RemappedKeysTests::openTextReadsItsOwnNewKeys()
{
TextCanvas shown;
remap({{"Text Editing:Finish editing text", ShortcutChord("\r", 4)}, {"Text Editing:Increase tracking", ShortcutChord("t", 2)}});
QTRY_VERIFY(shown.canvas->hasFocus());
shown.type("Hi");
const double tracking = shown.session.textDraft().value().style.tracking;
shown.key(Qt::Key_T, Qt::AltModifier);
QVERIFY(shown.session.textDraft().value().style.tracking > tracking);
// The old chords do nothing; the new one finishes.
shown.key(Qt::Key_Return, Qt::ControlModifier);
QVERIFY(shown.session.textDraft().has_value());
QCOMPARE(shown.content(), QString("Hi"));
shown.key(Qt::Key_Return, Qt::MetaModifier);
QVERIFY(!shown.session.textDraft());
}
void RemappedKeysTests::everySheetBindsSwiftsKeys()
{
remap({{"Canvas & Layers:Apply current canvas operation", ShortcutChord("k", 1)},
{"Canvas & Layers:Cancel current canvas operation", ShortcutChord("q", 1)},
{"Canvas & Layers:Toggle Levels preview", ShortcutChord("o", 2)}});
// The keys each sheet's live shortcuts answer to.
const auto keys = [](const QWidget &sheet) {
QSet<QString> result;
for (const QShortcut *shortcut : sheet.findChildren<QShortcut *>()) {
if (shortcut->isEnabled())
result.insert(shortcut->key().toString());
}
return result;
};
const QSet<QString> both{"Ctrl+K", "Ctrl+Q"};
EditorSession session;
session.createDocument(20, 20);
QImage red(20, 20, QImage::Format_RGBA8888_Premultiplied);
red.fill(Qt::red);
session.insert(ImportedImage(red, red, "Red"));
const CanvasDocument canvas = session.document().value();
QCOMPARE(keys(CanvasSizeSheet(canvas, PaletteColor::black(), PaletteColor::white(), [](std::optional<CanvasSizeOptions>) {})), both);
QCOMPARE(keys(ImageSizeSheet(canvas, [](std::optional<ImageSizeOptions>) {})), both);
QCOMPARE(keys(JPEGExportSheet(ExportRaster{red, 72}, [](std::optional<QByteArray>) {})), both);
QCOMPARE(keys(ColorPickerSheet(session, [](bool) {})), both);
QCOMPARE(keys(NewCanvasSheet(session, [](int, int) {}, [] {})), QSet<QString>{"Ctrl+K"});
QCOMPARE(keys(TransformInspector(session)), both);
QCOMPARE(keys(SelectionAmountSheet(session, SelectionAmountOperation::expand)), both);
session.addEffect(LayerEffectKind::stroke);
QCOMPARE(keys(EffectsSheet(session, LayerEffectKind::stroke)), both);
session.finishEffectsEditing(false);
session.beginLevels();
QCOMPARE(keys(LevelsSheet(session)), (QSet<QString>{"Ctrl+K", "Ctrl+Q", "Alt+O"}));
session.cancelLevels();
session.beginHueSaturation();
QCOMPARE(keys(HueSaturationSheet(session)), both);
session.cancelHueSaturation();
session.beginFilter(FilterKind::gaussianBlur);
QCOMPARE(keys(FilterSheet(session)), both);
session.cancelFilter();
}
void RemappedKeysTests::aKeyLeftAloneReachesTheParent()
{
EditorSession session;
session.createDocument(20, 20, true);
Parent parent;
auto *canvas = new CanvasView(session, &parent);
auto *list = new NativeLayerList(session, &parent);
list->move(0, 200);
parent.resize(300, 500);
parent.show();
QVERIFY(QTest::qWaitForWindowActive(&parent));
remap({{"Canvas & Layers:Brush tool", ShortcutChord("k")}});
QTest::keyClick(canvas, Qt::Key_Q);
QCOMPARE(parent.keys, 1);
QTest::keyClick(list, Qt::Key_Q);
QCOMPARE(parent.keys, 2);
// A key the canvas takes, or swallows, stays there.
QTest::keyClick(canvas, Qt::Key_K);
QTest::keyClick(canvas, Qt::Key_B);
QTest::keyClick(list, Qt::Key_K);
QCOMPARE(parent.keys, 2);
}
QTEST_MAIN(RemappedKeysTests)
#include "RemappedKeysTests.moc"