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
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(); });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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); });
|
||||
|
||||
@@ -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})
|
||||
|
||||
Reference in new issue
Block a user