14.6: the command palette on Ctrl+F

This commit is contained in:
ZacharyZhang-NY committed 2026-10-11 04:58:06 -04:00
1 parent 75fb546de8
commit e126762d5b
15 files changed
+1030 -3

No files matched your search

+236
View File
@@ -0,0 +1,236 @@
#include "UI/CommandPalette.h"
#include "Logging.h"
#include <QPointer>
#include <QTextBoundaryFinder>
#include <algorithm>
#include <map>
namespace {
// Swift's Characters: one string a grapheme.
QStringList characters(const QString &text)
{
QStringList result;
QTextBoundaryFinder finder(QTextBoundaryFinder::Grapheme, text);
qsizetype start = 0;
while (finder.toNextBoundary() != -1) {
result << text.mid(start, finder.position() - start);
start = finder.position();
}
return result;
}
char32_t first(const QString &character)
{
return character.toUcs4().value(0);
}
bool isLetter(const QString &character)
{
return QChar::isLetter(first(character));
}
// Each word starts a word of `title`, in order.
bool wordsStartWords(const QStringList &words, const QStringList &title)
{
qsizetype from = 0;
for (const QString &word : words) {
const QStringList wanted = characters(word);
std::optional<qsizetype> found;
for (qsizetype at = from; at + wanted.size() <= title.size() && !found; ++at)
if (std::equal(wanted.begin(), wanted.end(), title.begin() + at) && (at == 0 || !isLetter(title[at - 1])))
found = at + wanted.size();
if (!found)
return false;
from = found.value();
}
return true;
}
void collect(const QList<QAction *> &items, const QStringList &path, const std::set<QString> &skipping, std::map<QString, int> &seen,
std::vector<CommandPaletteEntry> &result)
{
for (QAction *item : items) {
const QString name = CommandPaletteMenu::plain(item->text());
if (item->isSeparator() || !item->isVisible() || skipping.contains(name) || name.isEmpty())
continue;
if (item->menu()) {
collect(item->menu()->actions(), path + QStringList{name}, skipping, seen, result);
continue;
}
const QString title = (path + QStringList{name}).join(QStringLiteral(" › "));
const int count = seen[title]++;
const QKeySequence key = item->shortcut();
result.push_back({.id = count == 0 ? title : QStringLiteral("%1 (%2)").arg(title).arg(count + 1),
.title = title,
.shortcut = key.isEmpty() ? std::nullopt : std::optional(key.toString(QKeySequence::NativeText)),
.isEnabled = item->isEnabled(),
.isOn = item->isChecked(),
.perform = [item = QPointer<QAction>(item), title] {
if (!item) {
qCWarning(lcApp) << "palette: menu entry gone:" << title;
return;
}
item->trigger();
}});
}
}
}
std::optional<int> CommandPaletteSearch::score(const QString &query, const QString &text)
{
QStringList wanted;
for (const QString &character : characters(query.toLower()))
if (!QChar::isSpace(first(character)))
wanted << character;
if (wanted.isEmpty())
return 0;
const QStringList letters = characters(text.toLower());
int total = 0;
qsizetype from = 0;
qsizetype previous = -2;
for (const QString &character : wanted) {
const qsizetype found = letters.indexOf(character, from);
if (found < 0)
return std::nullopt;
const bool wordStart = found == 0 || !(isLetter(letters[found - 1]) || QChar::isNumber(first(letters[found - 1])));
total += 1 + (wordStart ? 8 : 0) + (found == previous + 1 ? 5 : 0);
previous = found;
from = found + 1;
}
// How the text was found, best first; scattered letters last.
const QString title = letters.join(QString());
const QString phrase = trimmedWhitespaces(query.toLower());
const QStringList words = phrase.split(QLatin1Char(' '), Qt::SkipEmptyParts);
int tier = 1;
if (wordsStartWords({phrase}, letters))
tier = 5;
else if (words.size() > 1 && wordsStartWords(words, letters))
tier = 4;
else if (title.contains(phrase))
tier = 3;
else if (QString(title).remove(QLatin1Char(' ')).contains(wanted.join(QString())))
tier = 2;
// Same tier: shorter title wins; scattered letters by fall.
return tier * 1'000'000 + (tier == 1 ? total * 100 : 0) - int(letters.size() / 4);
}
std::vector<CommandPaletteEntry> CommandPaletteSearch::rank(const std::vector<CommandPaletteEntry> &entries, const QString &query)
{
struct Scored {
const CommandPaletteEntry *entry;
int score;
};
std::vector<Scored> scored;
for (const CommandPaletteEntry &entry : entries)
if (const std::optional<int> found = score(query, entry.title))
scored.push_back({&entry, found.value()});
std::stable_sort(scored.begin(), scored.end(), [&](const Scored &a, const Scored &b) {
if (a.entry->isEnabled != b.entry->isEnabled)
return a.entry->isEnabled;
return !query.isEmpty() && a.score > b.score;
});
std::vector<CommandPaletteEntry> result;
for (const Scored &each : scored)
result.push_back(*each.entry);
return result;
}
std::vector<CommandPaletteEntry> CommandPaletteMenu::entries(const QMenuBar &bar, const std::set<QString> &skipping)
{
std::map<QString, int> seen;
std::vector<CommandPaletteEntry> result;
collect(bar.actions(), {}, skipping, seen, result);
return result;
}
QString CommandPaletteMenu::plain(const QString &text)
{
QString result;
for (qsizetype at = 0; at < text.size(); ++at) {
if (text[at] != QLatin1Char('&'))
result += text[at];
else if (at + 1 < text.size() && text[at + 1] == QLatin1Char('&'))
result += text[++at];
}
return result;
}
std::vector<CommandPaletteEntry> CommandPaletteCommands::layerCommands(EditorSession &session)
{
const std::optional<ImageLayer> active = session.activeLayer();
const bool canAdd = session.canEditMask() && !(active && active->mask);
const bool selected = session.selection().has_value();
const auto add = [&session](bool revealing) { return [session = QPointer(&session), revealing] { if (session) session->addMask(revealing); }; };
const QString reveal = selected ? QStringLiteral("Layer › Add Layer Mask from Selection") : QStringLiteral("Layer › Add Layer Mask");
const QString hide = selected ? QStringLiteral("Layer › Add Layer Mask Hiding Selection") : QStringLiteral("Layer › Add Layer Mask (Hide All)");
return {{.id = reveal, .title = reveal, .shortcut = std::nullopt, .isEnabled = canAdd, .isOn = false, .perform = add(true)},
{.id = hide, .title = hide, .shortcut = std::nullopt, .isEnabled = canAdd, .isOn = false, .perform = add(false)}};
}
std::vector<CommandPaletteEntry> CommandPaletteCommands::tools(EditorSession &session)
{
using Setup = std::function<void(EditorSession &)>;
const std::vector<std::tuple<QString, QString, NavigationTool, Setup>> tools{
{"Move / Transform", "V", NavigationTool::move, nullptr},
{"Rectangular Marquee", "M", NavigationTool::marquee, [](EditorSession &s) { s.setMarqueeKind(LassoKind::rectangle); }},
{"Elliptical Marquee", "M", NavigationTool::marquee, [](EditorSession &s) { s.setMarqueeKind(LassoKind::ellipse); }},
{"Lasso", "L", NavigationTool::lasso, [](EditorSession &s) { s.setLassoKind(LassoKind::freehand); }},
{"Polygonal Lasso", "L", NavigationTool::lasso, [](EditorSession &s) { s.setLassoKind(LassoKind::polygonal); }},
{"Magic Wand", "W", NavigationTool::wand, [](EditorSession &s) { s.setWandMode(WandMode::wand); }},
{"Object Selection", "W", NavigationTool::wand, [](EditorSession &s) { s.setWandMode(WandMode::object); }},
{"Crop", "C", NavigationTool::crop, nullptr},
{"Brush", "B", NavigationTool::brush, [](EditorSession &s) { s.setBrushMode(BrushToolMode::paint); }},
{"Eraser", "E", NavigationTool::brush, [](EditorSession &s) { s.setBrushMode(BrushToolMode::erase); }},
{"Spot Healing Brush", "J", NavigationTool::spotHealing, nullptr},
{"Clone Stamp", "S", NavigationTool::cloneStamp, nullptr},
{"Liquify", "R", NavigationTool::blur, [](EditorSession &s) { s.setBlurMode(BlurToolMode::liquify); }},
{"Blur", "R", NavigationTool::blur, [](EditorSession &s) { s.setBlurMode(BlurToolMode::blur); }},
{"Smudge", "R", NavigationTool::blur, [](EditorSession &s) { s.setBlurMode(BlurToolMode::smudge); }},
{"Gradient", "G", NavigationTool::gradient, nullptr},
{"Rectangle", "U", NavigationTool::shape, [](EditorSession &s) { s.setShapeKind(ShapeKind::rectangle); }},
{"Ellipse", "U", NavigationTool::shape, [](EditorSession &s) { s.setShapeKind(ShapeKind::ellipse); }},
{"Line", "U", NavigationTool::shape, [](EditorSession &s) { s.setShapeKind(ShapeKind::line); }},
{"Type", "T", NavigationTool::type, nullptr},
{"Eyedropper", "I", NavigationTool::eyedropper, nullptr},
{"Hand", "H", NavigationTool::hand, nullptr},
{"Zoom", "Z", NavigationTool::zoom, nullptr}};
std::vector<CommandPaletteEntry> result;
for (const auto &[name, key, tool, setup] : tools) {
const QString id = QStringLiteral("Tool › %1").arg(name);
result.push_back({.id = id, .title = id, .shortcut = key, .isEnabled = session.document().has_value(), .isOn = false,
.perform = [session = QPointer(&session), tool, setup] {
if (!session)
return;
session->selectTool(tool);
// Only once the tool is in hand.
if (session->tool() == tool && setup)
setup(*session);
}});
}
return result;
}
void CommandPaletteModel::setQuery(const QString &query)
{
if (query != m_query)
selection = 0;
m_query = query;
}
std::optional<CommandPaletteEntry> CommandPaletteModel::selected() const
{
const std::vector<CommandPaletteEntry> found = results();
if (selection < 0 || selection >= int(found.size()))
return std::nullopt;
return found[size_t(selection)];
}
void CommandPaletteModel::move(int step)
{
const int count = int(results().size());
if (count == 0) {
selection = 0;
return;
}
selection = ((selection + step) % count + count) % count;
}
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#include "Document/EditorSession.h"
#include <QMenuBar>
#include <functional>
#include <optional>
#include <set>
#include <vector>
// One thing the palette runs: a menu command or tool.
struct CommandPaletteEntry {
// The title, numbered when an earlier entry shares it.
QString id;
// Its full path, shown and searched: "Filter › Gaussian Blur…".
QString title;
// Its current key, "Ctrl+Alt+P", or none.
std::optional<QString> shortcut;
// Disabled now: listed, greyed, never run.
bool isEnabled = true;
// A toggle on now, checked as in its menu.
bool isOn = false;
std::function<void()> perform;
};
// Fuzzy matching: letters in order, word starts and runs count.
namespace CommandPaletteSearch {
std::optional<int> score(const QString &query, const QString &text);
// Matches best first, enabled ahead; no query keeps menu order.
std::vector<CommandPaletteEntry> rank(const std::vector<CommandPaletteEntry> &entries, const QString &query);
}
// The menu bar as entries, read each time it opens.
namespace CommandPaletteMenu {
// `skipping` holds titles left out: whole menus or single entries.
std::vector<CommandPaletteEntry> entries(const QMenuBar &bar, const std::set<QString> &skipping);
// A menu title without its mnemonic: "Fil&ter" is Filter.
QString plain(const QString &text);
}
namespace CommandPaletteCommands {
// The Layers panel's mask button, so the palette finds it.
std::vector<CommandPaletteEntry> layerCommands(EditorSession &session);
// Every tool and each of its modes, by name.
std::vector<CommandPaletteEntry> tools(EditorSession &session);
}
// The query, the ranked results and which one is chosen.
class CommandPaletteModel {
public:
explicit CommandPaletteModel(std::vector<CommandPaletteEntry> entries) : m_entries(std::move(entries)) {}
const std::vector<CommandPaletteEntry> &entries() const { return m_entries; }
const QString &query() const { return m_query; }
// Only a real change goes back to the top.
void setQuery(const QString &query);
int selection = 0;
std::vector<CommandPaletteEntry> results() const { return CommandPaletteSearch::rank(m_entries, m_query); }
std::optional<CommandPaletteEntry> selected() const;
// Up and Down, wrapping at either end.
void move(int step);
private:
std::vector<CommandPaletteEntry> m_entries;
QString m_query;
};
+231
View File
@@ -0,0 +1,231 @@
#include "UI/CommandPaletteView.h"
#include "Logging.h"
#include <QKeyEvent>
#include <QLabel>
#include <QPainter>
#include <QApplication>
#include <QFontDatabase>
#include <QPainterPath>
#include <QVBoxLayout>
namespace {
constexpr int panelWidth = 460;
constexpr int panelHeight = 290;
constexpr int outer = 6;
QFont sized(QFont font, int pixels)
{
font.setPixelSize(pixels);
return font;
}
}
const std::set<QString> CommandPaletteController::skipped{QStringLiteral("Search Commands…")};
CommandPaletteRows::CommandPaletteRows(CommandPaletteModel &model, std::function<void(const CommandPaletteEntry &)> run, QWidget *parent)
: QWidget(parent), m_model(model), m_run(std::move(run))
{
setFont(sized(font(), 13));
}
int CommandPaletteRows::rowHeight() const
{
return fontMetrics().height() + 12;
}
QRect CommandPaletteRows::rowRect(int index) const
{
return QRect(outer, outer + index * rowHeight(), width() - 2 * outer, rowHeight());
}
void CommandPaletteRows::refresh()
{
m_shown = m_model.results();
setFixedHeight(outer + int(m_shown.size()) * rowHeight());
update();
}
void CommandPaletteRows::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
const QFont shortcutFont = sized(QFontDatabase::systemFont(QFontDatabase::FixedFont), 12);
for (int index = 0; index < int(m_shown.size()); ++index) {
const QRect row = rowRect(index);
if (!row.intersects(event->rect()))
continue;
const CommandPaletteEntry &entry = m_shown[size_t(index)];
if (index == m_model.selection) {
QColor chosen = palette().color(QPalette::Highlight);
chosen.setAlphaF(0.35);
QPainterPath round;
round.addRoundedRect(row, 6, 6);
painter.fillPath(round, chosen);
}
const QRect inside = row.adjusted(10, 0, -10, 0);
const QColor ink = palette().color(entry.isEnabled ? QPalette::Active : QPalette::Disabled, QPalette::WindowText);
int left = inside.left();
if (entry.isOn) {
// Swift's checkmark, twelve points with six after.
QPainterPath check;
const QPointF middle(left + 6, row.center().y() + 0.5);
check.moveTo(middle + QPointF(-4, 0));
check.lineTo(middle + QPointF(-1.5, 3));
check.lineTo(middle + QPointF(4, -3.5));
painter.setPen(QPen(ink, 1.6, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
painter.drawPath(check);
left += 12 + 6;
}
int right = inside.right();
if (entry.shortcut) {
painter.setFont(shortcutFont);
painter.setPen(palette().color(QPalette::PlaceholderText));
const int width = painter.fontMetrics().horizontalAdvance(entry.shortcut.value());
painter.drawText(QRect(right - width, row.top(), width + 1, row.height()), Qt::AlignVCenter | Qt::AlignRight, entry.shortcut.value());
right -= width + 6;
}
painter.setFont(font());
painter.setPen(ink);
const QRect text(left, row.top(), right - left, row.height());
painter.drawText(text, Qt::AlignVCenter | Qt::AlignLeft, fontMetrics().elidedText(entry.title, Qt::ElideRight, text.width()));
}
}
void CommandPaletteRows::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() != Qt::LeftButton)
return;
for (int index = 0; index < int(m_shown.size()); ++index)
if (rowRect(index).contains(event->position().toPoint())) {
m_run(m_shown[size_t(index)]);
return;
}
}
CommandPaletteView::CommandPaletteView(std::vector<CommandPaletteEntry> entries, std::function<void(const CommandPaletteEntry &)> run, QWidget *parent)
: QWidget(parent), m_model(std::move(entries)), m_run(std::move(run)), m_field(new QLineEdit(this)),
m_scroller(new QScrollArea(this)), m_rows(new CommandPaletteRows(m_model, m_run, m_scroller)), m_empty(new QLabel(QStringLiteral("No commands match"), this))
{
auto *column = new QVBoxLayout(this);
column->setContentsMargins(0, 0, 0, 0);
column->setSpacing(0);
m_field->setObjectName(QStringLiteral("commandSearch"));
m_field->setPlaceholderText(QStringLiteral("Search commands and tools"));
m_field->setFrame(false);
m_field->setFont(sized(m_field->font(), 17));
m_field->setTextMargins(16, 13, 16, 13);
m_field->installEventFilter(this);
column->addWidget(m_field);
auto *divider = new QWidget(this);
divider->setFixedHeight(1);
divider->setAutoFillBackground(true);
divider->setBackgroundRole(QPalette::Mid);
column->addWidget(divider);
// The list runs on under the panel's bottom edge.
m_scroller->setFrameShape(QFrame::NoFrame);
m_scroller->setWidgetResizable(true);
m_scroller->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_scroller->setWidget(m_rows);
column->addWidget(m_scroller, 1);
m_empty->setObjectName(QStringLiteral("noCommands"));
m_empty->setAlignment(Qt::AlignCenter);
m_empty->setForegroundRole(QPalette::PlaceholderText);
connect(m_field, &QLineEdit::textChanged, this, [this](const QString &text) {
m_model.setQuery(text);
refresh();
});
refresh();
}
void CommandPaletteView::refresh()
{
m_rows->refresh();
m_empty->setGeometry(m_scroller->geometry());
m_empty->setVisible(m_model.results().empty());
m_scroller->ensureVisible(0, m_rows->rowRect(m_model.selection).center().y(), 0, m_rows->rowHeight() / 2 + outer);
}
void CommandPaletteView::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
m_empty->setGeometry(m_scroller->geometry());
}
bool CommandPaletteView::eventFilter(QObject *watched, QEvent *event)
{
if (watched != m_field || event->type() != QEvent::KeyPress)
return QWidget::eventFilter(watched, event);
switch (static_cast<QKeyEvent *>(event)->key()) {
case Qt::Key_Up:
case Qt::Key_Down:
m_model.move(static_cast<QKeyEvent *>(event)->key() == Qt::Key_Up ? -1 : 1);
refresh();
return true;
case Qt::Key_Return:
case Qt::Key_Enter:
if (const std::optional<CommandPaletteEntry> entry = m_model.selected())
m_run(entry.value());
return true;
default:
return QWidget::eventFilter(watched, event);
}
}
void CommandPaletteController::toggle(EditorSession &session, const QMenuBar &bar, QAction *search)
{
if (isOpen()) {
close();
return;
}
std::vector<CommandPaletteEntry> entries = CommandPaletteMenu::entries(bar, skipped);
for (std::vector<CommandPaletteEntry> more : {CommandPaletteCommands::layerCommands(session), CommandPaletteCommands::tools(session)})
entries.insert(entries.end(), more.begin(), more.end());
if (!m_panel) {
// A popup: a click elsewhere or Escape puts it away.
m_panel = new QWidget(&m_window, Qt::Popup);
m_panel->setObjectName(QStringLiteral("commandPalette"));
m_panel->setFixedSize(panelWidth, panelHeight);
m_panel->setAutoFillBackground(true);
new QVBoxLayout(m_panel);
m_panel->layout()->setContentsMargins(0, 0, 0, 0);
m_panel->addAction(search);
m_panel->installEventFilter(this);
}
for (CommandPaletteView *old : m_panel->findChildren<CommandPaletteView *>(Qt::FindDirectChildrenOnly)) {
old->hide();
old->deleteLater();
}
auto *view = new CommandPaletteView(std::move(entries), [this](const CommandPaletteEntry &entry) { run(entry); }, m_panel);
m_panel->layout()->addWidget(view);
// A popup has no frame: its geometry is its place.
m_panel->setGeometry(QRect(m_window.frameGeometry().center() - QPoint(panelWidth / 2, panelHeight / 2), QSize(panelWidth, panelHeight)));
m_panel->show();
view->field().setFocus(Qt::PopupFocusReason);
qCInfo(lcApp) << "palette opened with" << view->model().entries().size() << "entries";
}
bool CommandPaletteController::eventFilter(QObject *watched, QEvent *event)
{
// Qt hands focus back only to the window's focus widget.
if (watched == m_panel && event->type() == QEvent::Hide && m_panel->isAncestorOf(QApplication::focusWidget()))
QApplication::focusWidget()->clearFocus();
return QObject::eventFilter(watched, event);
}
void CommandPaletteController::close()
{
if (m_panel)
m_panel->hide();
}
void CommandPaletteController::run(const CommandPaletteEntry &entry)
{
if (!entry.isEnabled) {
qCWarning(lcApp) << "palette: disabled:" << entry.title;
return;
}
close();
m_window.activateWindow();
qCInfo(lcApp) << "palette runs" << entry.title;
QMetaObject::invokeMethod(this, entry.perform, Qt::QueuedConnection);
}
+76
View File
@@ -0,0 +1,76 @@
#pragma once
#include "UI/CommandPalette.h"
#include <QLineEdit>
#include <QPointer>
#include <QScrollArea>
class QLabel;
// The ranked rows under the search field.
class CommandPaletteRows : public QWidget {
Q_OBJECT
public:
CommandPaletteRows(CommandPaletteModel &model, std::function<void(const CommandPaletteEntry &)> run, QWidget *parent);
int rowHeight() const;
// Where row `index` sits, inside the rows.
QRect rowRect(int index) const;
void refresh();
protected:
void paintEvent(QPaintEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
private:
CommandPaletteModel &m_model;
std::function<void(const CommandPaletteEntry &)> m_run;
std::vector<CommandPaletteEntry> m_shown;
};
// The palette: a search field over the ranked commands.
class CommandPaletteView : public QWidget {
Q_OBJECT
public:
CommandPaletteView(std::vector<CommandPaletteEntry> entries, std::function<void(const CommandPaletteEntry &)> run, QWidget *parent);
CommandPaletteModel &model() { return m_model; }
QLineEdit &field() { return *m_field; }
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
private:
void refresh();
CommandPaletteModel m_model;
std::function<void(const CommandPaletteEntry &)> m_run;
QLineEdit *m_field;
QScrollArea *m_scroller;
CommandPaletteRows *m_rows;
QLabel *m_empty;
};
// The palette over the window; a click elsewhere hides it.
class CommandPaletteController : public QObject {
Q_OBJECT
public:
// Left out of the palette: the palette itself.
static const std::set<QString> skipped;
explicit CommandPaletteController(QWidget &window) : m_window(window) {}
// A hidden palette lets go of the focus it held.
bool eventFilter(QObject *watched, QEvent *event) override;
bool isOpen() const { return m_panel && m_panel->isVisible(); }
QWidget *panel() const { return m_panel; }
// Opens over the window, or closes; `search` works inside.
void toggle(EditorSession &session, const QMenuBar &bar, QAction *search);
void close();
// Closes, gives the window back its keys, then runs it.
void run(const CommandPaletteEntry &entry);
private:
QWidget &m_window;
QPointer<QWidget> m_panel;
};
+3
View File
@@ -124,6 +124,9 @@ CompositorMenus::CompositorMenus(ProjectWorkspace &workspace, QMenuBar &bar, QWi
[this] { session().beginFilter(FilterKind::contentAwareFill); });
QMenu *view = bar.addMenu(QStringLiteral("&View"));
add(view, QStringLiteral("searchCommands"), QStringLiteral("Search Commands…"), QKeySequence(Qt::CTRL | Qt::Key_F),
[this, &bar] { m_palette.toggle(session(), bar, action(QStringLiteral("searchCommands"))); });
view->addSeparator();
add(view, QStringLiteral("fit"), QStringLiteral("Fit Canvas"), QKeySequence(Qt::CTRL | Qt::Key_0), [this] { session().fit(); });
add(view, QStringLiteral("actualPixels"), QStringLiteral("Actual Pixels"), QKeySequence(Qt::CTRL | Qt::Key_1), [this] { session().zoom(1); });
// Swift's stops; open text keeps the keys, as NSText does.
+2
View File
@@ -1,6 +1,7 @@
#pragma once
#include "Document/ProjectWorkspace.h"
#include "Rendering/EditorCanvas.h"
#include "UI/CommandPaletteView.h"
#include "UI/FloatingPanel.h"
#include <QAction>
#include <QApplication>
@@ -35,6 +36,7 @@ private:
ProjectWorkspace &m_workspace;
QWidget &m_window;
FloatingPanel m_shortcutsPanel{QStringLiteral("keyboardShortcuts"), m_window};
CommandPaletteController m_palette{m_window};
QMetaObject::Connection m_sessionWatch;
std::optional<QUuid> m_watchedTab;
// The field whose undo the Edit entries drive.
+1 -1
View File
@@ -125,7 +125,7 @@ const std::vector<ShortcutDefinition> &ShortcutDefinition::all()
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("Fit Canvas", "0", 1, true), entry("Search Commands", "f", 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),