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

+6
View File
@@ -1,4 +1,5 @@
#include "Rendering/EditorCanvas.h"
#include "UI/KeyboardShortcuts.h"
#include "Rendering/EyedropperIcon.h"
#include <QApplication>
#include <QKeyEvent>
@@ -314,6 +315,11 @@ bool CanvasView::eventFilter(QObject *watched, QEvent *event)
const auto *widget = qobject_cast<QWidget *>(watched);
if (!widget || widget->window() != window())
return false;
// Remapped keys stand for theirs; a moved key passes on.
const std::unique_ptr<QKeyEvent> typed = ShortcutSettings::shared().canvasEvent(*key);
if (!typed)
return false;
key = typed.get();
// Shift-+ and Shift-− step the blend mode, except while typing.
const bool typing = qobject_cast<QLineEdit *>(QApplication::focusWidget()) || (m_inlineTextEditor && hasFocus());
if (event->type() != QEvent::KeyPress || typing || key->modifiers() & (Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier))
+174
View File
@@ -0,0 +1,174 @@
#include "Rendering/EditorCanvas.h"
#include "UI/KeyboardShortcuts.h"
#include <QKeyEvent>
namespace {
bool isArrow(int key)
{
return key == Qt::Key_Left || key == Qt::Key_Right || key == Qt::Key_Up || key == Qt::Key_Down;
}
std::optional<NavigationTool> toolForKey(int key)
{
switch (key) {
case Qt::Key_B:
case Qt::Key_E:
return NavigationTool::brush;
case Qt::Key_J:
return NavigationTool::spotHealing;
case Qt::Key_S:
return NavigationTool::cloneStamp;
case Qt::Key_T:
return NavigationTool::type;
case Qt::Key_G:
return NavigationTool::gradient;
case Qt::Key_U:
return NavigationTool::shape;
case Qt::Key_I:
return NavigationTool::eyedropper;
case Qt::Key_W:
return NavigationTool::wand;
case Qt::Key_A:
return NavigationTool::idle;
case Qt::Key_R:
return NavigationTool::blur;
case Qt::Key_C:
return NavigationTool::crop;
case Qt::Key_V:
return NavigationTool::move;
case Qt::Key_H:
return NavigationTool::hand;
case Qt::Key_Z:
return NavigationTool::zoom;
default:
return std::nullopt;
}
}
}
void CanvasView::keyPressEvent(QKeyEvent *event)
{
// Remapped keys arrive as the keys they stand for.
ShortcutSettings &shortcuts = ShortcutSettings::shared();
const std::unique_ptr<QKeyEvent> typed = m_inlineTextEditor ? shortcuts.textEvent(*event) : shortcuts.canvasEvent(*event);
if (!typed)
return;
pressKey(typed.get(), event->key());
event->setAccepted(typed->isAccepted());
}
void CanvasView::pressKey(QKeyEvent *event, int physical)
{
// A drag swallows Alt's release: every key re-reads it.
m_optionHeld = event->modifiers().testFlag(Qt::AltModifier) || event->key() == Qt::Key_Alt;
m_controlHeld = event->modifiers().testFlag(Qt::ControlModifier) || event->key() == Qt::Key_Control;
m_shiftHeld = event->modifiers().testFlag(Qt::ShiftModifier) || event->key() == Qt::Key_Shift;
// Escape drops a text box being drawn, as Swift's first.
if (event->key() == Qt::Key_Escape && m_textBoxAnchor) {
endTextGesture();
return;
}
// Open text is the text view: it takes every key.
if (m_inlineTextEditor) {
if (!m_inlineTextEditor->keyPress(*event))
QWidget::keyPressEvent(event);
return;
}
const bool plain = !(event->modifiers() & (Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier));
const bool enter = event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter;
const std::optional<NavigationTool> tool = plain ? toolForKey(event->key()) : std::nullopt;
const bool erase = event->key() == Qt::Key_Backspace || event->key() == Qt::Key_Delete;
if (erase && event->modifiers() == Qt::ShiftModifier) {
// Shift-Delete: Content-Aware Fill, taken either way.
if (m_session.canContentAwareFill())
m_session.beginFilter(FilterKind::contentAwareFill);
} else if (m_session.levels() && event->key() != Qt::Key_Space) {
// Open Levels takes its keys and passes the rest on.
if (event->key() == Qt::Key_Escape)
m_session.cancelLevels();
else if (enter)
m_session.commitLevels();
else if (event->key() == Qt::Key_P && event->modifiers().testFlag(Qt::AltModifier))
m_session.updateLevels(m_session.levels()->settings, !m_session.levels()->preview);
else
QWidget::keyPressEvent(event);
} else if (m_session.brushStroke() || m_session.warpStroke()) {
// A stroke takes every key; Escape cancels it.
if (event->key() == Qt::Key_Escape && !m_session.isProjectBusy()) {
m_session.cancelBrush();
synchronizeDisplay();
}
} else if (selectionKey(*event)) {
return;
} else if (event->key() == Qt::Key_Escape && m_session.shapeDraft()) {
m_session.cancelShape();
synchronizeDisplay();
} else if ((event->key() == Qt::Key_Escape || enter) && m_session.gradientEdit()) {
m_gradientDrag.reset();
if (enter)
m_session.commitGradient();
else
m_session.cancelGradient();
} else if (cropKey(*event)) {
return;
} else if ((event->key() == Qt::Key_Escape || enter) && m_session.transformEdit()) {
// The keys end a drag with its edit, cursor included.
m_transformDrag.reset();
m_dragCursor.reset();
if (enter)
m_session.commitTransform();
else
m_session.cancelTransform();
updateCursor();
} else if (isArrow(event->key()) && event->modifiers().testFlag(Qt::ControlModifier) && !(event->modifiers() & (Qt::AltModifier | Qt::MetaModifier))
&& !m_session.lassoDraft() && m_session.selection() && !m_session.selection()->isEmpty()) {
// Ctrl-arrow moves the selected pixels in any tool.
const double step = event->modifiers().testFlag(Qt::ShiftModifier) ? 10 : 1;
m_session.nudgePixels(event->key() == Qt::Key_Left ? -step : event->key() == Qt::Key_Right ? step : 0,
event->key() == Qt::Key_Up ? -step : event->key() == Qt::Key_Down ? step : 0, [canvas = QPointer<CanvasView>(this)] {
if (canvas)
canvas->synchronizeDisplay();
});
} else if (m_session.tool() == NavigationTool::move && plain && isArrow(event->key())) {
const double step = event->modifiers().testFlag(Qt::ShiftModifier) ? 10 : 1;
m_session.nudgeLayer(event->key() == Qt::Key_Left ? -step : event->key() == Qt::Key_Right ? step : 0,
event->key() == Qt::Key_Up ? -step : event->key() == Qt::Key_Down ? step : 0);
} else if (erase && !(event->modifiers() & (Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier))) {
m_session.deleteKeyPressed();
} else if (event->key() == Qt::Key_Space) {
// Swift's panPhysicalKey: the key held, whatever it stands for.
m_panKey = physical;
m_spaceHeld = true;
updateCursor();
updateBrushCursor();
} else if (brushKey(*event)) {
return;
} else if (plain && (event->key() == Qt::Key_X || event->key() == Qt::Key_D)) {
// X swaps the palette; D brings back black and white.
if (event->key() == Qt::Key_X)
m_session.swapPaletteColors();
else
m_session.resetPaletteColors();
} else if (tool == NavigationTool::shape && event->modifiers().testFlag(Qt::ShiftModifier) && m_session.tool() == NavigationTool::shape) {
m_session.toggleShapeKind();
} else if (tool) {
m_session.selectTool(*tool);
} else {
QWidget::keyPressEvent(event);
}
}
void CanvasView::keyReleaseEvent(QKeyEvent *event)
{
// Auto-repeat sends release and press pairs: not a release.
if (event->isAutoRepeat()) {
QWidget::keyReleaseEvent(event);
} else if (event->key() == m_panKey.value_or(Qt::Key_Space)) {
m_panKey.reset();
m_spaceHeld = false;
updateCursor();
updateBrushCursor();
} else {
QWidget::keyReleaseEvent(event);
}
}
-157
View File
@@ -11,50 +11,6 @@
#include <QWindow>
#include <cmath>
namespace {
bool isArrow(int key)
{
return key == Qt::Key_Left || key == Qt::Key_Right || key == Qt::Key_Up || key == Qt::Key_Down;
}
std::optional<NavigationTool> toolForKey(int key)
{
switch (key) {
case Qt::Key_B:
case Qt::Key_E:
return NavigationTool::brush;
case Qt::Key_J:
return NavigationTool::spotHealing;
case Qt::Key_S:
return NavigationTool::cloneStamp;
case Qt::Key_T:
return NavigationTool::type;
case Qt::Key_G:
return NavigationTool::gradient;
case Qt::Key_U:
return NavigationTool::shape;
case Qt::Key_I:
return NavigationTool::eyedropper;
case Qt::Key_W:
return NavigationTool::wand;
case Qt::Key_A:
return NavigationTool::idle;
case Qt::Key_R:
return NavigationTool::blur;
case Qt::Key_C:
return NavigationTool::crop;
case Qt::Key_V:
return NavigationTool::move;
case Qt::Key_H:
return NavigationTool::hand;
case Qt::Key_Z:
return NavigationTool::zoom;
default:
return std::nullopt;
}
}
}
bool CanvasView::event(QEvent *event)
{
// Open text takes its editing keys ahead of the menus.
@@ -343,119 +299,6 @@ void CanvasView::mouseReleaseEvent(QMouseEvent *event)
synchronizeDisplay();
}
void CanvasView::keyPressEvent(QKeyEvent *event)
{
// A drag swallows Alt's release: every key re-reads it.
m_optionHeld = event->modifiers().testFlag(Qt::AltModifier) || event->key() == Qt::Key_Alt;
m_controlHeld = event->modifiers().testFlag(Qt::ControlModifier) || event->key() == Qt::Key_Control;
m_shiftHeld = event->modifiers().testFlag(Qt::ShiftModifier) || event->key() == Qt::Key_Shift;
// Escape drops a text box being drawn, as Swift's first.
if (event->key() == Qt::Key_Escape && m_textBoxAnchor) {
endTextGesture();
return;
}
// Open text is the text view: it takes every key.
if (m_inlineTextEditor) {
if (!m_inlineTextEditor->keyPress(*event))
QWidget::keyPressEvent(event);
return;
}
const bool plain = !(event->modifiers() & (Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier));
const bool enter = event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter;
const std::optional<NavigationTool> tool = plain ? toolForKey(event->key()) : std::nullopt;
const bool erase = event->key() == Qt::Key_Backspace || event->key() == Qt::Key_Delete;
if (erase && event->modifiers() == Qt::ShiftModifier) {
// Shift-Delete: Content-Aware Fill, taken either way.
if (m_session.canContentAwareFill())
m_session.beginFilter(FilterKind::contentAwareFill);
} else if (m_session.levels() && event->key() != Qt::Key_Space) {
// Open Levels takes its keys and passes the rest on.
if (event->key() == Qt::Key_Escape)
m_session.cancelLevels();
else if (enter)
m_session.commitLevels();
else if (event->key() == Qt::Key_P && event->modifiers().testFlag(Qt::AltModifier))
m_session.updateLevels(m_session.levels()->settings, !m_session.levels()->preview);
else
QWidget::keyPressEvent(event);
} else if (m_session.brushStroke() || m_session.warpStroke()) {
// A stroke takes every key; Escape cancels it.
if (event->key() == Qt::Key_Escape && !m_session.isProjectBusy()) {
m_session.cancelBrush();
synchronizeDisplay();
}
} else if (selectionKey(*event)) {
return;
} else if (event->key() == Qt::Key_Escape && m_session.shapeDraft()) {
m_session.cancelShape();
synchronizeDisplay();
} else if ((event->key() == Qt::Key_Escape || enter) && m_session.gradientEdit()) {
m_gradientDrag.reset();
if (enter)
m_session.commitGradient();
else
m_session.cancelGradient();
} else if (cropKey(*event)) {
return;
} else if ((event->key() == Qt::Key_Escape || enter) && m_session.transformEdit()) {
// The keys end a drag with its edit, cursor included.
m_transformDrag.reset();
m_dragCursor.reset();
if (enter)
m_session.commitTransform();
else
m_session.cancelTransform();
updateCursor();
} else if (isArrow(event->key()) && event->modifiers().testFlag(Qt::ControlModifier) && !(event->modifiers() & (Qt::AltModifier | Qt::MetaModifier))
&& !m_session.lassoDraft() && m_session.selection() && !m_session.selection()->isEmpty()) {
// Ctrl-arrow moves the selected pixels in any tool.
const double step = event->modifiers().testFlag(Qt::ShiftModifier) ? 10 : 1;
m_session.nudgePixels(event->key() == Qt::Key_Left ? -step : event->key() == Qt::Key_Right ? step : 0,
event->key() == Qt::Key_Up ? -step : event->key() == Qt::Key_Down ? step : 0, [canvas = QPointer<CanvasView>(this)] {
if (canvas)
canvas->synchronizeDisplay();
});
} else if (m_session.tool() == NavigationTool::move && plain && isArrow(event->key())) {
const double step = event->modifiers().testFlag(Qt::ShiftModifier) ? 10 : 1;
m_session.nudgeLayer(event->key() == Qt::Key_Left ? -step : event->key() == Qt::Key_Right ? step : 0,
event->key() == Qt::Key_Up ? -step : event->key() == Qt::Key_Down ? step : 0);
} else if (erase && !(event->modifiers() & (Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier))) {
m_session.deleteKeyPressed();
} else if (event->key() == Qt::Key_Space) {
m_spaceHeld = true;
updateCursor();
updateBrushCursor();
} else if (brushKey(*event)) {
return;
} else if (plain && (event->key() == Qt::Key_X || event->key() == Qt::Key_D)) {
// X swaps the palette; D brings back black and white.
if (event->key() == Qt::Key_X)
m_session.swapPaletteColors();
else
m_session.resetPaletteColors();
} else if (tool == NavigationTool::shape && event->modifiers().testFlag(Qt::ShiftModifier) && m_session.tool() == NavigationTool::shape) {
m_session.toggleShapeKind();
} else if (tool) {
m_session.selectTool(*tool);
} else {
QWidget::keyPressEvent(event);
}
}
void CanvasView::keyReleaseEvent(QKeyEvent *event)
{
// Auto-repeat sends release and press pairs: not a release.
if (event->isAutoRepeat()) {
QWidget::keyReleaseEvent(event);
} else if (event->key() == Qt::Key_Space) {
m_spaceHeld = false;
updateCursor();
updateBrushCursor();
} else {
QWidget::keyReleaseEvent(event);
}
}
void CanvasView::focusInEvent(QFocusEvent *event)
{
if (m_inlineTextEditor)
+4
View File
@@ -187,6 +187,8 @@ private:
bool moveSelectionTool(QPointF point, Qt::KeyboardModifiers modifiers);
void releaseSelectionTool();
bool selectionKey(const QKeyEvent &key);
// The keys as the canvas reads them, after remapping.
void pressKey(QKeyEvent *event, int physical);
void endSelectionGestures();
void dragSelection(QPointF point, Qt::KeyboardModifiers modifiers);
void dragMarqueeDraft(QPointF pixel, Qt::KeyboardModifiers modifiers);
@@ -255,6 +257,8 @@ private:
std::optional<NavigationTool> m_displayedTool;
int m_lastFocusRequest = 0;
bool m_spaceHeld = false;
// The key that began the pan, when Space is remapped.
std::optional<int> m_panKey;
bool m_optionHeld = false;
std::optional<QPointF> m_lastDragPoint;
std::optional<ZoomDrag> m_zoomDrag;
+3
View File
@@ -1,4 +1,5 @@
#include "UI/CanvasSizeSheet.h"
#include "UI/KeyboardShortcuts.h"
#include "UI/ByteCounts.h"
#include "UI/ColorPaletteControls.h"
#include "UI/ColorPickerSheet.h"
@@ -205,6 +206,8 @@ CanvasSizeSheet::CanvasSizeSheet(const CanvasDocument &document, PaletteColor fo
connect(cancel, &QPushButton::clicked, this, [this] { m_finish(std::nullopt); });
m_ok->setObjectName(QStringLiteral("canvasOK"));
m_ok->setDefault(true);
// Swift's configuredNativeShortcut: Return and Escape, as remapped.
NativeShortcut::bind(*this, m_ok, cancel);
// OK rests while the draft is invalid: no guard needed.
connect(m_ok, &QPushButton::clicked, this, [this] {
m_finish(CanvasSizeOptions{.width = qint64(std::round(m_draft.width)), .height = qint64(std::round(m_draft.height)), .anchor = m_anchor,
+3
View File
@@ -1,4 +1,5 @@
#include "UI/ColorPickerSheet.h"
#include "UI/KeyboardShortcuts.h"
#include <QFontDatabase>
#include <QGridLayout>
#include <QHBoxLayout>
@@ -183,6 +184,8 @@ ColorPickerSheet::ColorPickerSheet(EditorSession &session, std::function<void(bo
m_preview->setAccessibleName(QStringLiteral("New color"));
m_ok->setObjectName(QStringLiteral("pickerOK"));
m_ok->setDefault(true);
// Swift's configuredNativeShortcut: Return and Escape, as remapped.
NativeShortcut::bind(*this, m_ok, m_cancel);
m_ok->setFixedWidth(90);
m_cancel->setObjectName(QStringLiteral("pickerCancel"));
m_cancel->setAutoDefault(false);
+17 -1
View File
@@ -1,5 +1,6 @@
#include "UI/CompositorMenus.h"
#include "Logging.h"
#include "UI/KeyboardShortcuts.h"
#include <QApplication>
#include <QMenu>
@@ -88,6 +89,10 @@ CompositorMenus::CompositorMenus(ProjectWorkspace &workspace, QMenuBar &bar, QWi
qCWarning(lcApp) << "nothing to paste";
});
edit->addSeparator();
add(edit, QStringLiteral("keyboardShortcuts"), QStringLiteral("Keyboard Shortcuts…"), QKeySequence(), [this] {
m_shortcutsPanel.onClose = [this] { m_shortcutsPanel.close(); };
m_shortcutsPanel.show(QStringLiteral("Keyboard Shortcuts"), new KeyboardShortcutsSheet([this] { m_shortcutsPanel.close(); }));
});
// Text fields delete the selection, else back from the caret.
add(edit, QStringLiteral("fillForeground"), QStringLiteral("Fill with Foreground Color"), QKeySequence(Qt::ALT | Qt::Key_Backspace), [this] {
if (!m_field) {
@@ -215,6 +220,7 @@ CompositorMenus::CompositorMenus(ProjectWorkspace &workspace, QMenuBar &bar, QWi
connect(&m_workspace, &ProjectWorkspace::changed, this, &CompositorMenus::watchFront);
// A field gaining or losing focus changes Undo's meaning.
connect(qApp, &QApplication::focusChanged, this, &CompositorMenus::focusMoved);
connect(&ShortcutSettings::shared(), &ShortcutSettings::changed, this, &CompositorMenus::remap);
watchFront();
}
@@ -222,11 +228,21 @@ QAction *CompositorMenus::add(QMenu *menu, const QString &name, const QString &t
{
QAction *made = menu->addAction(text);
made->setObjectName(name);
made->setShortcut(shortcut);
made->setProperty("originalShortcut", shortcut);
made->setShortcut(ShortcutSettings::shared().menu(shortcut));
connect(made, &QAction::triggered, this, run);
return made;
}
void CompositorMenus::remap()
{
for (QAction *entry : parent()->findChildren<QAction *>()) {
const QVariant original = entry->property("originalShortcut");
if (original.isValid())
entry->setShortcut(ShortcutSettings::shared().menu(original.value<QKeySequence>()));
}
}
QAction *CompositorMenus::action(const QString &name) const
{
return parent()->findChild<QAction *>(name);
+5
View File
@@ -1,6 +1,7 @@
#pragma once
#include "Document/ProjectWorkspace.h"
#include "Rendering/EditorCanvas.h"
#include "UI/FloatingPanel.h"
#include <QAction>
#include <QLineEdit>
#include <QMenuBar>
@@ -24,8 +25,12 @@ private:
ProjectController &projects() const { return m_workspace.current().controller; }
QAction *add(QMenu *menu, const QString &name, const QString &text, const QKeySequence &shortcut, const std::function<void()> &run);
// Each entry's own key, remapped from ShortcutSettings.
void remap();
ProjectWorkspace &m_workspace;
QWidget &m_window;
FloatingPanel m_shortcutsPanel{QStringLiteral("keyboardShortcuts"), m_window};
QMetaObject::Connection m_sessionWatch;
std::optional<QUuid> m_watchedTab;
// The field whose undo the Edit entries drive.
+3
View File
@@ -1,4 +1,5 @@
#include "UI/EffectsSheet.h"
#include "UI/KeyboardShortcuts.h"
#include "UI/ColorPaletteControls.h"
#include "UI/ColorPickerSheet.h"
#include <QButtonGroup>
@@ -91,6 +92,8 @@ EffectsSheet::EffectsSheet(EditorSession &session, LayerEffectKind kind, QWidget
auto *ok = new QPushButton(QStringLiteral("OK"), this);
ok->setObjectName(QStringLiteral("effectsOK"));
ok->setDefault(true);
// Swift's configuredNativeShortcut: Return and Escape, as remapped.
NativeShortcut::bind(*this, ok, cancel);
connect(cancel, &QPushButton::clicked, this, [this] { m_session.finishEffectsEditing(false); });
connect(ok, &QPushButton::clicked, this, [this] { m_session.finishEffectsEditing(true); });
auto *buttons = new QHBoxLayout;
+3
View File
@@ -1,4 +1,5 @@
#include "UI/FilterSheet.h"
#include "UI/KeyboardShortcuts.h"
#include "UI/ColorPaletteControls.h"
#include "UI/ColorPickerSheet.h"
#include "UI/CurvesControls.h"
@@ -213,6 +214,8 @@ FilterSheet::FilterSheet(EditorSession &session, QWidget *parent)
m_cancel->setAutoDefault(false);
m_ok->setObjectName(QStringLiteral("filterOK"));
m_ok->setDefault(true);
// Swift's configuredNativeShortcut: Return and Escape, as remapped.
NativeShortcut::bind(*this, m_ok, m_cancel);
connect(m_preview, &QCheckBox::clicked, this, [this](bool on) { m_session.updateFilter(settings(), on); });
connect(m_cancel, &QPushButton::clicked, this, [this] { m_session.cancelFilter(); });
connect(m_ok, &QPushButton::clicked, this, [this] { m_session.commitFilter(); });
+3
View File
@@ -1,4 +1,5 @@
#include "UI/HueSaturationSheet.h"
#include "UI/KeyboardShortcuts.h"
#include "Rendering/EyedropperIcon.h"
#include "UI/ColorPickerSheet.h"
#include <QCheckBox>
@@ -322,6 +323,8 @@ HueSaturationSheet::HueSaturationSheet(EditorSession &session, QWidget *parent)
// Return is OK's from anywhere, as Swift's default action.
m_cancel->setAutoDefault(false);
m_ok->setDefault(true);
// Swift's configuredNativeShortcut: Return and Escape, as remapped.
NativeShortcut::bind(*this, m_ok, m_cancel);
connect(m_cancel, &QPushButton::clicked, this, [this] { m_session.cancelHueSaturation(); });
connect(m_ok, &QPushButton::clicked, this, [this] { m_session.commitHueSaturation(); });
auto *column = new QVBoxLayout(this);
+3
View File
@@ -1,4 +1,5 @@
#include "UI/ImageSizeSheet.h"
#include "UI/KeyboardShortcuts.h"
#include "UI/ColorPickerSheet.h"
#include <QCheckBox>
#include <QComboBox>
@@ -143,6 +144,8 @@ ImageSizeSheet::ImageSizeSheet(const CanvasDocument &document, std::function<voi
connect(cancel, &QPushButton::clicked, this, [this] { m_finish(std::nullopt); });
m_resize->setObjectName(QStringLiteral("imageResize"));
m_resize->setDefault(true);
// Swift's configuredNativeShortcut: Return and Escape, as remapped.
NativeShortcut::bind(*this, m_resize, cancel);
// Resize rests while the size is invalid: no guard needed.
connect(m_resize, &QPushButton::clicked, this, [this] {
m_finish(ImageSizeOptions{.width = qint64(std::round(m_width)), .height = qint64(std::round(m_height)), .resolution = m_resolution,
+3
View File
@@ -1,4 +1,5 @@
#include "UI/JPEGExportSheet.h"
#include "UI/KeyboardShortcuts.h"
#include "UI/ByteCounts.h"
#include "UI/ColorPaletteControls.h"
#include <QColorDialog>
@@ -148,6 +149,8 @@ JPEGExportSheet::JPEGExportSheet(ExportRaster raster, std::function<void(std::op
connect(cancel, &QPushButton::clicked, this, [this] { m_finish(std::nullopt); });
m_export->setObjectName(QStringLiteral("jpegExport"));
m_export->setDefault(true);
// Swift's configuredNativeShortcut: Return and Escape, as remapped.
NativeShortcut::bind(*this, m_export, cancel);
// Export rests until a result is ready.
connect(m_export, &QPushButton::clicked, this, [this] {
QSettings().setValue(qualityKey, m_options.quality);
+205
View File
@@ -0,0 +1,205 @@
#include "UI/KeyboardShortcuts.h"
#include <QApplication>
#include <QFrame>
#include <QHBoxLayout>
#include <QKeyEvent>
#include <QLabel>
#include <QLineEdit>
#include <QScrollArea>
#include <QVBoxLayout>
namespace {
QLabel *headline(const QString &text, QWidget *parent)
{
auto *label = new QLabel(text, parent);
QFont font = label->font();
font.setPixelSize(13);
font.setWeight(QFont::DemiBold);
label->setFont(font);
return label;
}
QLabel *words(const QString &text, QWidget *parent)
{
auto *label = new QLabel(text, parent);
label->setWordWrap(true);
return label;
}
QFrame *divider(QWidget *parent)
{
auto *line = new QFrame(parent);
line->setFrameShape(QFrame::HLine);
line->setForegroundRole(QPalette::Mid);
return line;
}
bool isModifier(int key)
{
return key == Qt::Key_Shift || key == Qt::Key_Control || key == Qt::Key_Alt || key == Qt::Key_Meta || key == Qt::Key_AltGr
|| key == Qt::Key_Super_L || key == Qt::Key_Super_R;
}
}
ShortcutRecorder::ShortcutRecorder(std::function<void()> start, std::function<void(ShortcutChord)> finish, QWidget *parent)
: QPushButton(parent), m_start(std::move(start)), m_finish(std::move(finish))
{
setAutoDefault(false);
setFixedSize(150, 26);
connect(this, &QPushButton::clicked, this, [this] {
setFocus();
m_start();
});
}
void ShortcutRecorder::display(const ShortcutChord &chord, bool recording)
{
m_recording = recording;
setText(recording ? QStringLiteral("Press keys…") : chord.label());
setAccessibleName(recording ? QStringLiteral("Press a shortcut") : chord.label());
}
// While recording every key is the recorder's: menus' and Tab.
bool ShortcutRecorder::event(QEvent *event)
{
if (m_recording && event->type() == QEvent::ShortcutOverride) {
event->accept();
return true;
}
if (m_recording && event->type() == QEvent::KeyPress) {
keyPressEvent(static_cast<QKeyEvent *>(event));
return true;
}
return QPushButton::event(event);
}
void ShortcutRecorder::keyPressEvent(QKeyEvent *event)
{
if (!m_recording) {
QPushButton::keyPressEvent(event);
return;
}
// A modifier alone is still being held down.
if (isModifier(event->key()))
return;
const ShortcutChord chord(*event);
if (chord.key.size() != 1) {
QApplication::beep();
return;
}
m_finish(chord);
clearFocus();
}
KeyboardShortcutsSheet::KeyboardShortcutsSheet(std::function<void()> close, QWidget *parent)
: QWidget(parent), m_close(std::move(close)), m_draft(ShortcutSettings::shared().overrides()), m_search(new QLineEdit(this)),
m_problem(new QLabel(this)), m_save(new QPushButton(QStringLiteral("Save"), this))
{
setObjectName(QStringLiteral("keyboardShortcutsSheet"));
setFixedWidth(660);
auto *column = new QVBoxLayout(this);
column->setContentsMargins(24, 24, 24, 24);
column->setSpacing(10);
QLabel *intro = words(QStringLiteral("Click a shortcut, then press its new key combination. Changes apply when you save."), this);
intro->setForegroundRole(QPalette::PlaceholderText);
column->addWidget(intro);
m_search->setObjectName(QStringLiteral("shortcutSearch"));
m_search->setPlaceholderText(QStringLiteral("Search shortcuts"));
column->addWidget(m_search);
auto *scroll = new QScrollArea(this);
scroll->setFixedHeight(465);
scroll->setWidgetResizable(true);
scroll->setFrameShape(QFrame::NoFrame);
auto *list = new QWidget(scroll);
auto *rows = new QVBoxLayout(list);
rows->setContentsMargins(0, 0, 8, 0);
rows->setSpacing(6);
for (const QString &group : {QStringLiteral("Menus"), QStringLiteral("Canvas & Layers"), QStringLiteral("Text Editing")}) {
rows->addSpacing(8);
rows->addWidget(headline(group, list));
for (const ShortcutDefinition &definition : ShortcutDefinition::all()) {
if (definition.group != group)
continue;
const QString id = definition.id();
auto *row = new QWidget(list);
auto *line = new QHBoxLayout(row);
line->setContentsMargins(0, 0, 0, 0);
line->addWidget(new QLabel(definition.title, row));
line->addStretch(1);
auto *recorder = new ShortcutRecorder([this, id] {
m_recording = id;
synchronize();
}, [this, id](const ShortcutChord &chord) {
m_draft.insert(id, chord);
m_recording = std::nullopt;
synchronize();
}, row);
recorder->setObjectName(QStringLiteral("recorder:") + id);
line->addWidget(recorder);
rows->addWidget(row);
m_rows.push_back({row, recorder, &definition});
}
}
rows->addWidget(divider(list));
rows->addWidget(headline(QStringLiteral("Contextual keys & mouse gestures"), list));
rows->addWidget(words(QStringLiteral("Text fields keep their standard editing keys. Dialogs share the Apply and Cancel assignments above. "
"Numeric fields use Up and Down, with Shift for larger steps. The shortcut editor itself always "
"uses Return to save and Esc to cancel when not recording."), list));
rows->addWidget(words(QStringLiteral("Alt temporarily selects the eyedropper in painting tools. Shift constrains shapes and movement or adds to "
"a selection; Alt subtracts from selections or draws from the centre. Ctrl-drag moves selected pixels; "
"Ctrl-Alt-drag copies them. Alt-drag duplicates layers, folders and effects; Alt-click at a layer boundary "
"toggles clipping. Ctrl-click a thumbnail loads its selection. Ctrl bypasses snapping. Right-drag adjusts "
"brush size. Modifier-and-mouse gestures are fixed."), list));
rows->addStretch(1);
scroll->setWidget(list);
column->addWidget(scroll);
m_problem->setObjectName(QStringLiteral("shortcutProblem"));
m_problem->setForegroundRole(QPalette::BrightText);
QFont callout = m_problem->font();
callout.setPixelSize(12);
m_problem->setFont(callout);
m_problem->setWordWrap(true);
column->addWidget(m_problem);
column->addWidget(divider(this));
auto *buttons = new QHBoxLayout;
auto *restore = new QPushButton(QStringLiteral("Restore Defaults"), this);
restore->setObjectName(QStringLiteral("restoreShortcuts"));
restore->setAutoDefault(false);
auto *cancel = new QPushButton(QStringLiteral("Cancel"), this);
cancel->setObjectName(QStringLiteral("cancelShortcuts"));
cancel->setAutoDefault(false);
m_save->setObjectName(QStringLiteral("saveShortcuts"));
m_save->setDefault(true);
buttons->addWidget(restore);
buttons->addStretch(1);
buttons->addWidget(cancel);
buttons->addWidget(m_save);
column->addLayout(buttons);
connect(m_search, &QLineEdit::textChanged, this, &KeyboardShortcutsSheet::synchronize);
connect(restore, &QPushButton::clicked, this, [this] {
m_recording = std::nullopt;
m_draft.clear();
synchronize();
});
connect(cancel, &QPushButton::clicked, this, [this] { m_close(); });
connect(m_save, &QPushButton::clicked, this, [this] {
if (ShortcutSettings::shared().save(m_draft))
m_close();
});
synchronize();
}
void KeyboardShortcutsSheet::synchronize()
{
const QString search = m_search->text();
for (const Row &each : m_rows) {
const QString id = each.definition->id();
each.row->setVisible(search.isEmpty() || each.definition->title.contains(search, Qt::CaseInsensitive));
each.recorder->display(m_draft.value(id, each.definition->original), m_recording == id);
}
// Only a conflict takes room above the buttons.
const std::optional<QString> problem = ShortcutSettings::problem(m_draft);
m_problem->setText(problem.value_or(QString()));
m_problem->setVisible(problem.has_value());
m_save->setEnabled(!m_recording && !problem);
}
+351
View File
@@ -0,0 +1,351 @@
#include "UI/KeyboardShortcuts.h"
#include "Logging.h"
#include <QAbstractButton>
#include <QJsonDocument>
#include <QJsonObject>
#include <QKeyEvent>
#include <QSettings>
#include <QShortcut>
#include <array>
namespace {
// Swift's special keys, their Qt keys and their names.
struct Special {
QString key;
Qt::Key qt;
};
const std::array specials{
Special{QStringLiteral("\x7f"), Qt::Key_Backspace}, Special{QStringLiteral("\r"), Qt::Key_Return},
Special{QStringLiteral("\x1b"), Qt::Key_Escape}, Special{QStringLiteral("\t"), Qt::Key_Tab},
Special{QStringLiteral(" "), Qt::Key_Space}, Special{QString(QChar(0xf702)), Qt::Key_Left},
Special{QString(QChar(0xf703)), Qt::Key_Right}, Special{QString(QChar(0xf701)), Qt::Key_Down},
Special{QString(QChar(0xf700)), Qt::Key_Up},
};
int bits(Qt::KeyboardModifiers modifiers)
{
return (modifiers.testFlag(Qt::ControlModifier) ? 1 : 0) | (modifiers.testFlag(Qt::AltModifier) ? 2 : 0)
| (modifiers.testFlag(Qt::MetaModifier) ? 4 : 0) | (modifiers.testFlag(Qt::ShiftModifier) ? 8 : 0);
}
Qt::KeyboardModifiers flags(int bits)
{
Qt::KeyboardModifiers result;
result.setFlag(Qt::ControlModifier, bits & 1);
result.setFlag(Qt::AltModifier, bits & 2);
result.setFlag(Qt::MetaModifier, bits & 4);
result.setFlag(Qt::ShiftModifier, bits & 8);
return result;
}
// A key as Swift's charactersIgnoringModifiers reads it, lowered.
QString keyText(int key)
{
if (key == Qt::Key_Delete || key == Qt::Key_Backspace)
return QStringLiteral("\x7f");
if (key == Qt::Key_Enter)
return QStringLiteral("\r");
if (key == Qt::Key_Backtab)
return QStringLiteral("\t");
for (const Special &special : specials) {
if (special.qt == key)
return special.key;
}
const QString typed = QKeySequence(key).toString(QKeySequence::PortableText).toLower();
// Shifted brackets and signs count as their own keys.
static const QHash<QString, QString> unshifted{{QStringLiteral("{"), QStringLiteral("[")}, {QStringLiteral("}"), QStringLiteral("]")},
{QStringLiteral("+"), QStringLiteral("=")}, {QStringLiteral("_"), QStringLiteral("-")}};
return unshifted.value(typed, typed);
}
// Swift's shifted characters for the keys Shift changes.
QString shiftedText(const QString &key, bool shifted)
{
static const QHash<QString, QString> shifts{{QStringLiteral("["), QStringLiteral("{")}, {QStringLiteral("]"), QStringLiteral("}")},
{QStringLiteral("="), QStringLiteral("+")}, {QStringLiteral("-"), QStringLiteral("_")}};
return shifted ? shifts.value(key, key) : key;
}
Qt::Key qtKey(const QString &key, bool shifted)
{
for (const Special &special : specials) {
if (special.key == key)
return special.qt;
}
const QString typed = shiftedText(key, shifted);
return typed.size() == 1 ? Qt::Key(typed.at(0).toUpper().unicode()) : Qt::Key_unknown;
}
ShortcutDefinition entry(const QString &title, const QString &key, int modifiers, bool menu)
{
return {title, menu ? QStringLiteral("Menus") : QStringLiteral("Canvas & Layers"), ShortcutChord(key, modifiers)};
}
QJsonObject encoded(const QHash<QString, ShortcutChord> &values)
{
QJsonObject object;
for (auto value = values.cbegin(); value != values.cend(); ++value)
object.insert(value.key(), QJsonObject{{QStringLiteral("key"), value->key}, {QStringLiteral("modifiers"), value->modifiers}});
return object;
}
}
ShortcutChord::ShortcutChord(const QKeyEvent &event) : key(keyText(event.key())), modifiers(bits(event.modifiers())) {}
ShortcutChord::ShortcutChord(QKeyCombination combination)
: key(keyText(combination.key())), modifiers(bits(combination.keyboardModifiers()))
{
}
QKeyCombination ShortcutChord::combination() const
{
return QKeyCombination(flags(modifiers), qtKey(key, modifiers & 8));
}
QString ShortcutChord::label() const
{
return QKeySequence(combination()).toString(QKeySequence::NativeText);
}
std::unique_ptr<QKeyEvent> ShortcutChord::event(const QKeyEvent &like) const
{
const Qt::Key typed = qtKey(key, modifiers & 8);
const QString text = typed == Qt::Key_Space || key.size() != 1 || key.at(0).unicode() >= 0xf700 || key.at(0).unicode() < 0x20
? QString()
: shiftedText(key, modifiers & 8);
return std::make_unique<QKeyEvent>(like.type(), typed, flags(modifiers), text, like.isAutoRepeat(), like.count());
}
const std::vector<ShortcutDefinition> &ShortcutDefinition::all()
{
static const std::vector<ShortcutDefinition> definitions = [] {
const QString backspace = QStringLiteral("\x7f");
// Swift's list; Hide Compositor has no twin on Linux.
std::vector<ShortcutDefinition> result{
entry("Undo", "z", 1, true), entry("Redo", "z", 9, true), entry("New Canvas", "n", 1, true),
entry("Open Project", "o", 1, true), entry("Save", "s", 1, true), entry("Save As", "s", 9, true),
entry("Export PNG", "e", 9, true), entry("Export JPEG", "s", 11, true), entry("Close Project", "w", 1, true),
entry("Fit Canvas", "0", 1, true), entry("Actual Pixels", "1", 1, true), entry("Zoom In", "=", 1, true),
entry("Zoom Out", "-", 1, true), entry("Show Transform Controls", "h", 1, true), entry("Cut", "x", 1, true),
entry("Copy", "c", 1, true), entry("Copy Merged", "c", 9, true), entry("Paste", "v", 1, true),
entry("Fill with Foreground", backspace, 2, true), entry("Fill with Background", backspace, 1, true),
entry("Content-Aware Fill", backspace, 8, true), entry("Select All", "a", 1, true), entry("Deselect", "d", 1, true),
entry("Inverse Selection", "i", 9, true), entry("Select Subject", "a", 3, true), entry("Curves", "m", 1, true),
entry("Levels", "l", 1, true), entry("Hue/Saturation", "u", 1, true), entry("Invert Pixels / Mask", "i", 1, true),
entry("Canvas Size", "c", 3, true), entry("Image Size", "i", 3, true), entry("Transform Layer / Selection", "t", 1, true),
entry("Duplicate / Layer via Copy", "j", 1, true), entry("Toggle Clipping Mask", "g", 3, true),
entry("Group Layers", "g", 1, true), entry("New Blank Layer", "n", 9, true), entry("Move Layer Up", "]", 1, true),
entry("Move Layer Down", "[", 1, true), entry("Merge Layers", "e", 1, true)};
const std::vector<std::pair<const char *, QString>> tools{
{"Select tool", "a"}, {"Move / Transform tool", "v"}, {"Hand tool", "h"}, {"Zoom tool", "z"}, {"Brush tool", "b"},
{"Eraser", "e"}, {"Spot Healing", "j"}, {"Clone Stamp", "s"}, {"Type tool", "t"}, {"Gradient tool", "g"},
{"Shape tool", "u"}, {"Eyedropper tool", "i"}, {"Marquee / cycle shape", "m"}, {"Magic Wand", "w"},
{"Lasso / cycle mode", "l"}, {"Blur / Smudge / Liquify", "r"}, {"Crop tool", "c"}, {"Swap foreground/background", "x"},
{"Reset colors", "d"}, {"Cycle tool mode", "\t"}, {"Temporary Hand tool (hold)", " "},
{"Delete selection / layer / effect / lasso point", backspace}, {"Apply current canvas operation", "\r"},
{"Cancel current canvas operation", "\x1b"}, {"Decrease brush size", "["}, {"Increase brush size", "]"}};
for (const auto &[title, key] : tools)
result.push_back(entry(title, key, 0, false));
for (const auto &[title, key] : std::vector<std::pair<const char *, const char *>>{
{"Decrease brush hardness", "["}, {"Increase brush hardness", "]"}, {"Previous blend mode", "-"}, {"Next blend mode", "="}, {"Cycle shape kind", "u"}})
result.push_back(entry(title, key, 8, false));
for (int digit = 0; digit <= 9; ++digit)
result.push_back(entry(QStringLiteral("Opacity digit %1 (type two for exact %)").arg(digit), QString::number(digit), 0, false));
for (const auto &[direction, key] : std::vector<std::pair<QString, QString>>{
{"Left", QString(QChar(0xf702))}, {"Right", QString(QChar(0xf703))}, {"Up", QString(QChar(0xf700))}, {"Down", QString(QChar(0xf701))}}) {
result.push_back(entry(QStringLiteral("Nudge %1 1 px").arg(direction), key, 0, false));
result.push_back(entry(QStringLiteral("Nudge %1 10 px").arg(direction), key, 8, false));
result.push_back(entry(QStringLiteral("Move selected pixels %1 1 px").arg(direction), key, 1, false));
result.push_back(entry(QStringLiteral("Move selected pixels %1 10 px").arg(direction), key, 9, false));
}
const QString text = QStringLiteral("Text Editing");
result.push_back({QStringLiteral("Finish editing text"), text, ShortcutChord(QStringLiteral("\r"), 1)});
for (const auto &[title, key] : std::vector<std::pair<QString, QString>>{{"Decrease tracking", QString(QChar(0xf702))},
{"Increase tracking", QString(QChar(0xf703))},
{"Decrease leading", QString(QChar(0xf700))},
{"Increase leading", QString(QChar(0xf701))}}) {
result.push_back({title, text, ShortcutChord(key, 2)});
result.push_back({title + QStringLiteral(" by 10"), text, ShortcutChord(key, 10)});
}
result.push_back(entry("Toggle Levels preview", "p", 2, false));
return result;
}();
return definitions;
}
ShortcutSettings &ShortcutSettings::shared()
{
static ShortcutSettings settings;
return settings;
}
ShortcutSettings::ShortcutSettings()
{
reload();
}
void ShortcutSettings::reload()
{
m_overrides.clear();
const QByteArray data = QSettings().value(QLatin1String(storageKey)).toByteArray();
if (!data.isEmpty()) {
QHash<QString, ShortcutChord> saved;
const QJsonObject object = QJsonDocument::fromJson(data).object();
for (auto value = object.constBegin(); value != object.constEnd(); ++value) {
const QJsonObject chord = value->toObject();
saved.insert(value.key(), ShortcutChord(chord.value(QLatin1String("key")).toString(), chord.value(QLatin1String("modifiers")).toInt(-1)));
}
// Swift keeps stored overrides only while they hold together.
if (const std::optional<QString> wrong = problem(saved))
qCWarning(lcApp).noquote() << "stored keyboard shortcuts ignored:" << *wrong;
else
m_overrides = saved;
}
emit changed();
}
ShortcutChord ShortcutSettings::chord(const ShortcutDefinition &definition) const
{
return m_overrides.value(definition.id(), definition.original);
}
QKeySequence ShortcutSettings::menu(const QKeySequence &original) const
{
// An empty sequence reads as no key, which nothing matches.
const ShortcutChord typed(original[0]);
for (const ShortcutDefinition &definition : ShortcutDefinition::all()) {
if (definition.isMenu() && definition.original == typed)
return QKeySequence(chord(definition).combination());
}
return original;
}
ShortcutChord ShortcutSettings::native(const ShortcutChord &original) const
{
for (const ShortcutDefinition &definition : ShortcutDefinition::all()) {
if (!definition.isMenu() && definition.original == original)
return chord(definition);
}
return original;
}
bool ShortcutSettings::save(const QHash<QString, ShortcutChord> &values)
{
if (problem(values))
return false;
m_overrides = values;
QSettings().setValue(QLatin1String(storageKey), QJsonDocument(encoded(values)).toJson(QJsonDocument::Compact));
emit changed();
return true;
}
std::optional<QString> ShortcutSettings::problem(const QHash<QString, ShortcutChord> &values)
{
QHash<QString, QString> assigned;
for (const ShortcutDefinition &definition : ShortcutDefinition::all()) {
const ShortcutChord chord = values.value(definition.id(), definition.original);
if (chord.key.size() != 1 || chord.modifiers < 0 || chord.modifiers > 15)
return QStringLiteral("Choose a single key with optional modifiers.");
if (definition.group == QLatin1String("Text Editing") && !(chord.modifiers & 7))
return QStringLiteral("Text-editing shortcuts need Ctrl, Alt or Meta so they do not replace normal typing.");
const QString name = QString::number(chord.modifiers) + QLatin1Char(':') + chord.key;
if (assigned.contains(name))
return QStringLiteral("%1 is assigned to both %2 and %3.").arg(chord.label(), assigned.value(name), definition.title);
assigned.insert(name, definition.title);
}
return std::nullopt;
}
std::unique_ptr<QKeyEvent> ShortcutSettings::canvasEvent(const QKeyEvent &event) const
{
std::unique_ptr<QKeyEvent> same(event.clone());
if (m_overrides.isEmpty())
return same;
const ShortcutChord input(event);
const std::vector<ShortcutDefinition> &all = ShortcutDefinition::all();
for (const ShortcutDefinition &definition : all) {
if (definition.group == QLatin1String("Canvas & Layers") && chord(definition) == input)
return definition.original == input ? std::move(same) : definition.original.event(event);
}
for (const ShortcutDefinition &definition : all) {
if (definition.group != QLatin1String("Text Editing") && definition.original == input && chord(definition) != input)
return nullptr;
}
// Tool letters also take Shift, unless Shift has its own.
if (input.modifiers == 8) {
const ShortcutChord plain(input.key);
for (const ShortcutDefinition &definition : all) {
if (!definition.isMenu() && definition.original.modifiers == 0 && chord(definition) == plain)
return ShortcutChord(definition.original.key, 8).event(event);
}
for (const ShortcutDefinition &definition : all) {
if (!definition.isMenu() && definition.original == plain && chord(definition) != plain)
return nullptr;
}
}
return same;
}
std::unique_ptr<QKeyEvent> ShortcutSettings::textEvent(const QKeyEvent &event) const
{
std::unique_ptr<QKeyEvent> same(event.clone());
if (m_overrides.isEmpty())
return same;
std::vector<ShortcutDefinition> definitions;
for (const ShortcutDefinition &definition : ShortcutDefinition::all()) {
if (definition.group == QLatin1String("Text Editing") || definition.original == ShortcutChord(QStringLiteral("\x1b")))
definitions.push_back(definition);
}
const ShortcutChord input(event);
for (const ShortcutDefinition &definition : definitions) {
if (chord(definition) == input)
return definition.original == input ? std::move(same) : definition.original.event(event);
}
for (const ShortcutDefinition &definition : definitions) {
if (definition.original == input && chord(definition) != input)
return nullptr;
}
return same;
}
NativeShortcut::NativeShortcut(const ShortcutChord &original, QWidget &scope, QAbstractButton &target)
: QObject(&scope), m_original(original), m_scope(scope), m_target(&target), m_shortcut(new QShortcut(&scope))
{
m_shortcut->setContext(Qt::WidgetWithChildrenShortcut);
// A disabled button ignores click() itself.
connect(m_shortcut, &QShortcut::activated, this, [this] {
if (m_target && m_target->isVisible())
m_target->click();
});
scope.installEventFilter(this);
connect(&ShortcutSettings::shared(), &ShortcutSettings::changed, this, &NativeShortcut::synchronize);
synchronize();
}
void NativeShortcut::bind(QWidget &scope, QAbstractButton *apply, QAbstractButton *cancel)
{
if (apply)
new NativeShortcut(ShortcutChord(QStringLiteral("\r")), scope, *apply);
if (cancel)
new NativeShortcut(ShortcutChord(QStringLiteral("\x1b")), scope, *cancel);
}
void NativeShortcut::synchronize()
{
const ShortcutChord chord = ShortcutSettings::shared().native(m_original);
m_shortcut->setKey(QKeySequence(chord.combination()));
// Its own plain Return or Escape already reaches the button.
const bool plain = !m_original.modifiers && (m_original.key == QLatin1String("\r") || m_original.key == QLatin1String("\x1b"));
m_shortcut->setEnabled(!(plain && chord == m_original));
}
bool NativeShortcut::eventFilter(QObject *watched, QEvent *event)
{
// The original key, moved elsewhere, no longer acts.
if (watched == &m_scope && event->type() == QEvent::KeyPress) {
const ShortcutChord typed(*static_cast<QKeyEvent *>(event));
if (typed == m_original && ShortcutSettings::shared().native(m_original) != m_original)
return true;
}
return QObject::eventFilter(watched, event);
}
+133
View File
@@ -0,0 +1,133 @@
#pragma once
#include <QHash>
#include <QKeySequence>
#include <QObject>
#include <QPointer>
#include <QPushButton>
#include <QString>
#include <functional>
#include <memory>
#include <optional>
#include <vector>
class QAbstractButton;
class QKeyEvent;
class QLabel;
class QLineEdit;
class QShortcut;
class QWidget;
// One key with its modifiers, stored as Swift stores it.
struct ShortcutChord {
// One character; Backspace is 0x7f, the arrows Cocoa's F700s.
QString key;
// Bits: Ctrl 1, Alt 2, Meta 4, Shift 8.
int modifiers = 0;
ShortcutChord() = default;
ShortcutChord(QString key, int modifiers = 0) : key(std::move(key)), modifiers(modifiers) {}
explicit ShortcutChord(const QKeyEvent &event);
explicit ShortcutChord(QKeyCombination combination);
QKeyCombination combination() const;
QString label() const;
// The same press, as though this chord were typed.
std::unique_ptr<QKeyEvent> event(const QKeyEvent &like) const;
friend bool operator==(const ShortcutChord &, const ShortcutChord &) = default;
};
struct ShortcutDefinition {
QString title;
QString group;
ShortcutChord original;
QString id() const { return group + QLatin1Char(':') + title; }
bool isMenu() const { return group == QLatin1String("Menus"); }
static const std::vector<ShortcutDefinition> &all();
};
// Swift's ShortcutSettings: overrides, kept in QSettings.
class ShortcutSettings : public QObject {
Q_OBJECT
public:
static ShortcutSettings &shared();
static constexpr const char *storageKey = "keyboardShortcuts.v1";
const QHash<QString, ShortcutChord> &overrides() const { return m_overrides; }
ShortcutChord chord(const ShortcutDefinition &definition) const;
// A menu entry's key, or a sheet's, as remapped.
QKeySequence menu(const QKeySequence &original) const;
ShortcutChord native(const ShortcutChord &original) const;
// Saves valid overrides; answers whether it did.
bool save(const QHash<QString, ShortcutChord> &values);
static std::optional<QString> problem(const QHash<QString, ShortcutChord> &values);
// Null swallows the press; else the press to handle.
std::unique_ptr<QKeyEvent> canvasEvent(const QKeyEvent &event) const;
std::unique_ptr<QKeyEvent> textEvent(const QKeyEvent &event) const;
// Reads the stored overrides again, as at launch.
void reload();
signals:
void changed();
private:
ShortcutSettings();
QHash<QString, ShortcutChord> m_overrides;
};
// Swift's configuredNativeShortcut on a sheet's button.
class NativeShortcut : public QObject {
Q_OBJECT
public:
NativeShortcut(const ShortcutChord &original, QWidget &scope, QAbstractButton &target);
// A sheet's default (Return) and cancel (Escape) buttons.
static void bind(QWidget &scope, QAbstractButton *apply, QAbstractButton *cancel);
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
private:
void synchronize();
const ShortcutChord m_original;
QWidget &m_scope;
QPointer<QAbstractButton> m_target;
QShortcut *const m_shortcut;
};
// A shortcut's button: click, then press the new keys.
class ShortcutRecorder : public QPushButton {
Q_OBJECT
public:
ShortcutRecorder(std::function<void()> start, std::function<void(ShortcutChord)> finish, QWidget *parent);
void display(const ShortcutChord &chord, bool recording);
protected:
bool event(QEvent *event) override;
void keyPressEvent(QKeyEvent *event) override;
private:
const std::function<void()> m_start;
const std::function<void(ShortcutChord)> m_finish;
bool m_recording = false;
};
// Swift's KeyboardShortcutsSheet, in the "keyboardShortcuts" panel.
class KeyboardShortcutsSheet : public QWidget {
Q_OBJECT
public:
KeyboardShortcutsSheet(std::function<void()> close, QWidget *parent = nullptr);
private:
void synchronize();
const std::function<void()> m_close;
QHash<QString, ShortcutChord> m_draft;
std::optional<QString> m_recording;
QLineEdit *const m_search;
QLabel *const m_problem;
QPushButton *const m_save;
struct Row {
QWidget *row;
ShortcutRecorder *recorder;
const ShortcutDefinition *definition;
};
std::vector<Row> m_rows;
};
+3
View File
@@ -1,4 +1,5 @@
#include "UI/LassoControls.h"
#include "UI/KeyboardShortcuts.h"
#include <QBoxLayout>
#include <QFrame>
#include <QLabel>
@@ -277,6 +278,8 @@ SelectionAmountSheet::SelectionAmountSheet(EditorSession &session, SelectionAmou
cancel->setAutoDefault(false);
m_ok->setObjectName(QStringLiteral("amountOK"));
m_ok->setDefault(true);
// Swift's configuredNativeShortcut: Return and Escape, as remapped.
NativeShortcut::bind(*this, m_ok, cancel);
buttons->addWidget(cancel);
buttons->addStretch(1);
buttons->addWidget(m_ok);
+4 -1
View File
@@ -1,4 +1,5 @@
#include "UI/LevelsSheet.h"
#include "UI/KeyboardShortcuts.h"
#include "Rendering/EyedropperIcon.h"
#include <QEvent>
#include <QFrame>
@@ -291,7 +292,7 @@ LevelsSheet::LevelsSheet(EditorSession &session, QWidget *parent)
connect(m_autos[index], &QPushButton::clicked, this, [this, index] { m_session.autoLevels(allLevelsAutos[index]); });
}
m_sampleHint->setWordWrap(true);
m_preview->setShortcut(QKeySequence(Qt::ALT | Qt::Key_P));
new NativeShortcut(ShortcutChord(QStringLiteral("p"), 2), *this, *m_preview);
connect(m_preview, &QCheckBox::clicked, this, [this](bool on) { m_session.updateLevels(m_session.levels().value().settings, on); });
m_reset->setAutoDefault(false);
connect(m_reset, &QPushButton::clicked, this, [this] {
@@ -300,6 +301,8 @@ LevelsSheet::LevelsSheet(EditorSession &session, QWidget *parent)
});
m_cancel->setAutoDefault(false);
m_ok->setDefault(true);
// Swift's configuredNativeShortcut: Return and Escape, as remapped.
NativeShortcut::bind(*this, m_ok, m_cancel);
connect(m_cancel, &QPushButton::clicked, this, [this] { m_session.cancelLevels(); });
connect(m_ok, &QPushButton::clicked, this, [this] { m_session.commitLevels(); });
m_spinner->setRange(0, 0);
+11
View File
@@ -1,4 +1,5 @@
#include "UI/NativeLayerList.h"
#include "UI/KeyboardShortcuts.h"
#include <QEvent>
#include <QKeyEvent>
#include <QScrollBar>
@@ -168,6 +169,16 @@ bool NativeLayerList::event(QEvent *event)
}
void NativeLayerList::keyPressEvent(QKeyEvent *event)
{
// Remapped keys arrive as the keys they stand for.
const std::unique_ptr<QKeyEvent> typed = ShortcutSettings::shared().canvasEvent(*event);
if (!typed)
return;
pressKey(typed.get());
event->setAccepted(typed->isAccepted());
}
void NativeLayerList::pressKey(QKeyEvent *event)
{
const bool plain = !(event->modifiers() & (Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier));
const int key = event->key();
+1
View File
@@ -238,6 +238,7 @@ public:
protected:
bool event(QEvent *event) override;
void keyPressEvent(QKeyEvent *event) override;
void pressKey(QKeyEvent *event);
// Below every row: Swift's table lets go of everything.
void mousePressEvent(QMouseEvent *event) override;
void changeEvent(QEvent *event) override;
+12 -1
View File
@@ -1,4 +1,5 @@
#include "UI/NewCanvasSheet.h"
#include "UI/KeyboardShortcuts.h"
#include <QApplication>
#include <QBuffer>
#include <QClipboard>
@@ -32,6 +33,8 @@ NewCanvasSheet::NewCanvasSheet(EditorSession &session, std::function<void(int, i
m_note->setObjectName(QStringLiteral("canvasNote"));
m_create->setObjectName(QStringLiteral("createCanvas"));
m_create->setDefault(true);
// Swift's configuredNativeShortcut: Return and Escape, as remapped.
NativeShortcut::bind(*this, m_create, nullptr);
auto *open = new QPushButton(QStringLiteral("Open project"), this);
open->setObjectName(QStringLiteral("openProject"));
auto *import = new QPushButton(QStringLiteral("Import image"), this);
@@ -70,8 +73,16 @@ NewCanvasSheet::NewCanvasSheet(EditorSession &session, std::function<void(int, i
connect(m_width, &QLineEdit::textChanged, this, &NewCanvasSheet::validate);
connect(m_height, &QLineEdit::textChanged, this, &NewCanvasSheet::validate);
// Return anywhere on the sheet is the default action.
std::vector<QShortcut *> returns;
for (const Qt::Key key : {Qt::Key_Return, Qt::Key_Enter})
new QShortcut(QKeySequence(key), this, this, &NewCanvasSheet::create, Qt::WidgetWithChildrenShortcut);
returns.push_back(new QShortcut(QKeySequence(key), this, this, &NewCanvasSheet::create, Qt::WidgetWithChildrenShortcut));
// Until Apply is moved elsewhere, as Swift's native shortcut.
const auto follow = [returns] {
for (QShortcut *shortcut : returns)
shortcut->setEnabled(ShortcutSettings::shared().native(ShortcutChord(QStringLiteral("\r"))) == ShortcutChord(QStringLiteral("\r")));
};
connect(&ShortcutSettings::shared(), &ShortcutSettings::changed, this, follow);
follow();
connect(m_create, &QPushButton::clicked, this, &NewCanvasSheet::create);
connect(open, &QPushButton::clicked, this, [onOpen = std::move(onOpen)] { onOpen(); });
connect(import, &QPushButton::clicked, this, [this] { m_session.setShowsImporter(true); });
+3
View File
@@ -1,4 +1,5 @@
#include "UI/TransformInspector.h"
#include "UI/KeyboardShortcuts.h"
#include "UI/LayerIcons.h"
#include <QEvent>
#include <QKeyEvent>
@@ -147,6 +148,8 @@ TransformInspector::TransformInspector(EditorSession &session, QWidget *parent)
m_cancel->setObjectName(QStringLiteral("cancelTransform"));
connect(m_cancel, &QPushButton::clicked, this, [this] { m_session.cancelTransform(); });
m_apply->setObjectName(QStringLiteral("applyTransform"));
// Swift's configuredNativeShortcut: Return and Escape, as remapped.
NativeShortcut::bind(*this, m_apply, m_cancel);
connect(m_apply, &QPushButton::clicked, this, [this] { m_session.commitTransform(); });
// Before the stretch ending the row, in Swift's order.
for (QWidget *widget : std::initializer_list<QWidget *>{m_autoSelect, m_showControls, m_fields, m_cancel, m_apply})