12.2b: the Keyboard Shortcuts window and remapped keys everywhere (Compositor 1.1.6)
This commit is contained in:
1 parent
f1b2c65c1e
commit
4bfc6c237e
29 files changed
+1645
-164
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
Reference in new issue
Block a user