453 lines
30 KiB
C++
453 lines
30 KiB
C++
#include "UI/CompositorMenus.h"
|
|
#include "IO/RecentProjects.h"
|
|
#include "Logging.h"
|
|
#include "UI/KeyboardShortcuts.h"
|
|
#include "UI/NavigatorMinimap.h"
|
|
#include "UI/ProjectWorkspaceView.h"
|
|
#include <QApplication>
|
|
#include <QMenu>
|
|
|
|
namespace {
|
|
// An entry's name: Hue/Saturation is newHueSaturationAdjustment.
|
|
QString adjustmentName(AdjustmentKind kind)
|
|
{
|
|
return QStringLiteral("new%1Adjustment").arg(rawValue(kind).remove(QLatin1Char(' ')).remove(QLatin1Char('/')).remove(QLatin1Char('&')));
|
|
}
|
|
|
|
// A filter's entry from Swift's title: gaussianBlur.
|
|
QString filterName(FilterKind kind)
|
|
{
|
|
QString name = rawValue(kind);
|
|
name.remove(QLatin1Char(' ')).remove(QLatin1Char('&'));
|
|
name[0] = name[0].toLower();
|
|
return name;
|
|
}
|
|
}
|
|
|
|
CompositorMenus::CompositorMenus(ProjectWorkspace &workspace, QMenuBar &bar, ProjectWorkspaceView &window)
|
|
: QObject(&bar), m_workspace(workspace), m_window(window), m_view(window)
|
|
{
|
|
QMenu *file = bar.addMenu(QStringLiteral("&File"));
|
|
add(file, QStringLiteral("newCanvas"), QStringLiteral("New Canvas…"), QKeySequence(Qt::CTRL | Qt::Key_N), [this] { projects().newCanvas(); });
|
|
add(file, QStringLiteral("openProject"), QStringLiteral("Open Project…"), QKeySequence(Qt::CTRL | Qt::Key_O), [this] { projects().open(); });
|
|
QMenu *recent = file->addMenu(QStringLiteral("Open Recent"));
|
|
recent->menuAction()->setObjectName(QStringLiteral("openRecent"));
|
|
listRecent(recent);
|
|
connect(&RecentProjects::shared(), &RecentProjects::changed, this, [this, recent] { listRecent(recent); });
|
|
add(file, QStringLiteral("importImages"), QStringLiteral("Import Images…"), QKeySequence(), [this] { session().setShowsImporter(true); });
|
|
file->addSeparator();
|
|
add(file, QStringLiteral("save"), QStringLiteral("Save"), QKeySequence(Qt::CTRL | Qt::Key_S), [this] { projects().save(); });
|
|
add(file, QStringLiteral("saveAs"), QStringLiteral("Save As…"), QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_S), [this] { projects().save(true); });
|
|
file->addSeparator();
|
|
add(file, QStringLiteral("exportPNG"), QStringLiteral("Export PNG…"), QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_E), [this] { projects().exportPNG(); });
|
|
add(file, QStringLiteral("exportJPEG"), QStringLiteral("Export JPEG…"), QKeySequence(Qt::CTRL | Qt::ALT | Qt::SHIFT | Qt::Key_S),
|
|
[this] { projects().exportAs(ExportFormat::jpeg); });
|
|
// Swift 1.4.8: Photoshop's Ctrl+Alt+Shift+W.
|
|
add(file, QStringLiteral("exportAs"), QStringLiteral("Export As…"), QKeySequence(Qt::CTRL | Qt::ALT | Qt::SHIFT | Qt::Key_W), [this] { projects().exportAs(); });
|
|
file->addSeparator();
|
|
add(file, QStringLiteral("closeProject"), QStringLiteral("Close Project"), QKeySequence(Qt::CTRL | Qt::Key_W), [this] { projects().close(&m_window); });
|
|
|
|
QMenu *edit = bar.addMenu(QStringLiteral("&Edit"));
|
|
add(edit, QStringLiteral("undo"), QStringLiteral("Undo"), QKeySequence(Qt::CTRL | Qt::Key_Z), [this] {
|
|
if (m_field)
|
|
m_field->undo();
|
|
else if (InlineTextEditor *text = typedText())
|
|
text->undo();
|
|
else
|
|
session().undo();
|
|
});
|
|
add(edit, QStringLiteral("redo"), QStringLiteral("Redo"), QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_Z), [this] {
|
|
if (m_field)
|
|
m_field->redo();
|
|
else if (InlineTextEditor *text = typedText())
|
|
text->redo();
|
|
else
|
|
session().redo();
|
|
});
|
|
edit->addSeparator();
|
|
// Cut, Copy and Paste check when chosen, as Swift's do.
|
|
add(edit, QStringLiteral("cut"), QStringLiteral("Cut"), QKeySequence(Qt::CTRL | Qt::Key_X), [this] {
|
|
if (m_field)
|
|
m_field->cut();
|
|
else if (InlineTextEditor *text = typedText())
|
|
text->cut();
|
|
else if (session().selection() && session().canCopyPixels())
|
|
session().cutSelection();
|
|
else
|
|
qCWarning(lcApp) << "nothing to cut";
|
|
});
|
|
add(edit, QStringLiteral("copy"), QStringLiteral("Copy"), QKeySequence(Qt::CTRL | Qt::Key_C), [this] {
|
|
if (m_field)
|
|
m_field->copy();
|
|
else if (InlineTextEditor *text = typedText())
|
|
text->copy();
|
|
else if (session().canCopyPixels() || session().canCopyLayer())
|
|
session().copySelection();
|
|
else
|
|
qCWarning(lcApp) << "nothing to copy";
|
|
});
|
|
add(edit, QStringLiteral("copyMerged"), QStringLiteral("Copy Merged"), QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_C), [this] { session().copyMergedSelection(); });
|
|
add(edit, QStringLiteral("paste"), QStringLiteral("Paste"), QKeySequence(Qt::CTRL | Qt::Key_V), [this] {
|
|
if (m_field)
|
|
m_field->paste();
|
|
else if (InlineTextEditor *text = typedText())
|
|
text->paste();
|
|
else if (m_workspace.pasteCopiedLayer())
|
|
return;
|
|
else if (session().canPaste())
|
|
session().paste();
|
|
else
|
|
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) {
|
|
session().fillSelection(EditorSession::FillSource::foreground);
|
|
return;
|
|
}
|
|
if (!m_field->hasSelectedText())
|
|
m_field->cursorWordBackward(true);
|
|
m_field->backspace();
|
|
});
|
|
add(edit, QStringLiteral("fillBackground"), QStringLiteral("Fill with Background Color"), QKeySequence(Qt::CTRL | Qt::Key_Backspace), [this] {
|
|
if (!m_field) {
|
|
session().fillSelection(EditorSession::FillSource::background);
|
|
return;
|
|
}
|
|
if (!m_field->hasSelectedText())
|
|
m_field->home(true);
|
|
m_field->backspace();
|
|
});
|
|
add(edit, QStringLiteral("clearSelectionPixels"), QStringLiteral("Clear Selection Pixels"), QKeySequence(), [this] { session().clearSelectedPixels(); });
|
|
add(edit, QStringLiteral("contentAwareFill"), QStringLiteral("Content-Aware Fill…"), QKeySequence(Qt::SHIFT | Qt::Key_Backspace),
|
|
[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"))); });
|
|
// A plain F; a field takes its letters first.
|
|
add(view, QStringLiteral("toggleFullscreen"), QStringLiteral("Toggle Fullscreen"), QKeySequence(Qt::Key_F), [this] { m_view.toggleCanvasOnly(); })
|
|
->setCheckable(true);
|
|
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.
|
|
const auto zoomBy = [this](int step) {
|
|
if (!m_field && !(m_canvas && session().textDraft()))
|
|
session().zoomKeyboard(step);
|
|
};
|
|
add(view, QStringLiteral("zoomIn"), QStringLiteral("Zoom In"), QKeySequence(Qt::CTRL | Qt::Key_Equal), [zoomBy] { zoomBy(1); });
|
|
add(view, QStringLiteral("zoomOut"), QStringLiteral("Zoom Out"), QKeySequence(Qt::CTRL | Qt::Key_Minus), [zoomBy] { zoomBy(-1); });
|
|
add(view, QStringLiteral("navigator"), QStringLiteral("Navigator (300% and above)"), QKeySequence(), [this] {
|
|
NavigatorMinimap::setVisibleSetting(!NavigatorMinimap::visibleSetting());
|
|
session().notify();
|
|
})->setCheckable(true);
|
|
add(view, QStringLiteral("pixelGrid"), QStringLiteral("Pixel Grid (800% and above)"), QKeySequence(), [this] { session().setShowsPixelGrid(!session().showsPixelGrid()); })
|
|
->setCheckable(true);
|
|
// Ctrl+H: the Move tool's box; the desktop hides apps.
|
|
add(view, QStringLiteral("transformControls"), QStringLiteral("Show Transform Controls"), QKeySequence(Qt::CTRL | Qt::Key_H),
|
|
[this] { session().setShowsTransformControls(!session().showsTransformControls()); })
|
|
->setCheckable(true);
|
|
// Swift 1.1.7's guides, grid, rulers and Snap To.
|
|
view->addSeparator();
|
|
const auto toggle = [this](QMenu *menu, const QString &name, const QString &text, const QKeySequence &key, bool (EditorSession::*read)() const,
|
|
void (EditorSession::*write)(bool)) {
|
|
add(menu, name, text, key, [this, read, write] { (session().*write)(!(session().*read)()); })->setCheckable(true);
|
|
};
|
|
QMenu *show = view->addMenu(QStringLiteral("Show"));
|
|
toggle(show, QStringLiteral("showGrid"), QStringLiteral("Grid"), QKeySequence(Qt::CTRL | Qt::Key_Apostrophe), &EditorSession::showsGrid,
|
|
&EditorSession::setShowsGrid);
|
|
toggle(show, QStringLiteral("showGuides"), QStringLiteral("Guides"), QKeySequence(Qt::CTRL | Qt::Key_Semicolon), &EditorSession::showsGuides,
|
|
&EditorSession::setShowsGuides);
|
|
add(view, QStringLiteral("gridSettings"), QStringLiteral("Grid Settings…"), QKeySequence(), [this] { projects().gridSettings(); });
|
|
toggle(view, QStringLiteral("showRulers"), QStringLiteral("Rulers"), QKeySequence(Qt::CTRL | Qt::Key_R), &EditorSession::showsRulers,
|
|
&EditorSession::setShowsRulers);
|
|
view->addSeparator();
|
|
toggle(view, QStringLiteral("snapEnabled"), QStringLiteral("Snap"), QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_Semicolon),
|
|
&EditorSession::snapEnabled, &EditorSession::setSnapEnabled);
|
|
QMenu *snapTo = view->addMenu(QStringLiteral("Snap To"));
|
|
toggle(snapTo, QStringLiteral("snapToGuides"), QStringLiteral("Guides"), QKeySequence(), &EditorSession::snapToGuides, &EditorSession::setSnapToGuides);
|
|
toggle(snapTo, QStringLiteral("snapToGrid"), QStringLiteral("Grid"), QKeySequence(), &EditorSession::snapToGrid, &EditorSession::setSnapToGrid);
|
|
toggle(snapTo, QStringLiteral("snapToLayers"), QStringLiteral("Layers"), QKeySequence(), &EditorSession::snapToLayers, &EditorSession::setSnapToLayers);
|
|
toggle(snapTo, QStringLiteral("snapToDocumentBounds"), QStringLiteral("Document Bounds"), QKeySequence(), &EditorSession::snapToDocumentBounds,
|
|
&EditorSession::setSnapToDocumentBounds);
|
|
view->addSeparator();
|
|
toggle(view, QStringLiteral("lockGuides"), QStringLiteral("Lock Guides"), QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_Semicolon),
|
|
&EditorSession::locksGuides, &EditorSession::setLocksGuides);
|
|
add(view, QStringLiteral("clearGuides"), QStringLiteral("Clear Guides"), QKeySequence(), [this] { session().clearGuides(); });
|
|
|
|
QMenu *select = bar.addMenu(QStringLiteral("&Select"));
|
|
// The focus takes Select All first, as Swift's responder chain.
|
|
add(select, QStringLiteral("selectAll"), QStringLiteral("All"), QKeySequence(Qt::CTRL | Qt::Key_A), [this] {
|
|
if (m_field)
|
|
m_field->selectAll();
|
|
else if (InlineTextEditor *text = typedText())
|
|
text->selectAll();
|
|
else
|
|
session().selectAll();
|
|
});
|
|
add(select, QStringLiteral("deselect"), QStringLiteral("Deselect"), QKeySequence(Qt::CTRL | Qt::Key_D), [this] { session().deselect(); });
|
|
add(select, QStringLiteral("inverse"), QStringLiteral("Inverse"), QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_I), [this] { session().invertSelection(); });
|
|
add(select, QStringLiteral("layerPixels"), QStringLiteral("Layer's Pixels"), QKeySequence(), [this] {
|
|
if (const std::optional<QUuid> id = session().activeLayerID())
|
|
session().loadLayerSelection(*id);
|
|
});
|
|
add(select, QStringLiteral("subject"), QStringLiteral("Subject"), QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_A),
|
|
[this] { session().selectSubject(SelectionMode::replace, {}); });
|
|
add(select, QStringLiteral("colorRange"), QStringLiteral("Color Range…"), QKeySequence(), [this] { session().beginColorRange(); });
|
|
add(select, QStringLiteral("maskBlackAreas"), QStringLiteral("Mask's Black Areas"), QKeySequence(), [this] {
|
|
if (const std::optional<QUuid> id = session().activeLayerID())
|
|
session().loadMaskSelection(*id);
|
|
});
|
|
select->addSeparator();
|
|
add(select, QStringLiteral("expandSelection"), QStringLiteral("Expand…"), QKeySequence(),
|
|
[this] { session().promptSelectionAmount(SelectionAmountOperation::expand); });
|
|
add(select, QStringLiteral("contractSelection"), QStringLiteral("Contract…"), QKeySequence(),
|
|
[this] { session().promptSelectionAmount(SelectionAmountOperation::contract); });
|
|
add(select, QStringLiteral("featherSelection"), QStringLiteral("Feather…"), QKeySequence(),
|
|
[this] { session().promptSelectionAmount(SelectionAmountOperation::feather); });
|
|
|
|
QMenu *image = bar.addMenu(QStringLiteral("&Image"));
|
|
add(image, QStringLiteral("curves"), QStringLiteral("Curves…"), QKeySequence(Qt::CTRL | Qt::Key_M), [this] { session().beginFilter(FilterKind::curves); });
|
|
add(image, QStringLiteral("levels"), QStringLiteral("Levels…"), QKeySequence(Qt::CTRL | Qt::Key_L), [this] { session().beginLevels(); });
|
|
add(image, QStringLiteral("hueSaturation"), QStringLiteral("Hue/Saturation…"), QKeySequence(Qt::CTRL | Qt::Key_U),
|
|
[this] { session().beginHueSaturation(); });
|
|
for (const FilterKind kind : {FilterKind::blackWhite, FilterKind::colorBalance, FilterKind::exposure, FilterKind::gradientMap, FilterKind::grain})
|
|
add(image, filterName(kind), rawValue(kind).replace(QLatin1Char('&'), QStringLiteral("&&")) + QStringLiteral("…"), QKeySequence(), [this, kind] { session().beginFilter(kind); });
|
|
add(image, QStringLiteral("invert"), QStringLiteral("Invert"), QKeySequence(Qt::CTRL | Qt::Key_I), [this] { session().invertPixels(); });
|
|
image->addSeparator();
|
|
add(image, QStringLiteral("canvasSize"), QStringLiteral("Canvas Size…"), QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_C), [this] { projects().canvasSize(); });
|
|
add(image, QStringLiteral("imageSize"), QStringLiteral("Image Size…"), QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_I), [this] { projects().imageSize(); });
|
|
add(image, QStringLiteral("trim"), QStringLiteral("Trim…"), QKeySequence(), [this] { projects().trim(); });
|
|
image->addSeparator();
|
|
add(image, QStringLiteral("rotateCanvasClockwise"), QStringLiteral("Rotate Canvas 90° Clockwise"), QKeySequence(), [this] { session().rotateCanvas(true); });
|
|
add(image, QStringLiteral("rotateCanvasCounterclockwise"), QStringLiteral("Rotate Canvas 90° Counterclockwise"), QKeySequence(),
|
|
[this] { session().rotateCanvas(false); });
|
|
add(image, QStringLiteral("flipCanvasHorizontal"), QStringLiteral("Flip Canvas Horizontal"), QKeySequence(), [this] { session().flipCanvas(true); });
|
|
add(image, QStringLiteral("flipCanvasVertical"), QStringLiteral("Flip Canvas Vertical"), QKeySequence(), [this] { session().flipCanvas(false); });
|
|
|
|
QMenu *filter = bar.addMenu(QStringLiteral("Fil&ter"));
|
|
// Ctrl+Meta+F, Swift's Cmd+Control: Ctrl+F searches commands.
|
|
add(filter, QStringLiteral("lastFilter"), QStringLiteral("Last Filter"), QKeySequence(Qt::CTRL | Qt::META | Qt::Key_F),
|
|
[this] { session().repeatLastFilter(); });
|
|
filter->addSeparator();
|
|
for (const FilterKind kind : allFilterKinds) {
|
|
if (kind != FilterKind::contentAwareFill && !isImageAdjustment(kind))
|
|
add(filter, filterName(kind), rawValue(kind) + QStringLiteral("…"), QKeySequence(), [this, kind] { session().beginFilter(kind); });
|
|
}
|
|
|
|
QMenu *layer = bar.addMenu(QStringLiteral("&Layer"));
|
|
QMenu *adjustments = layer->addMenu(QStringLiteral("New Adjustment Layer"));
|
|
adjustments->menuAction()->setObjectName(QStringLiteral("newAdjustmentLayer"));
|
|
for (const AdjustmentKind kind : allAdjustmentKinds)
|
|
// A menu reads a lone ampersand as a mnemonic: doubled.
|
|
add(adjustments, adjustmentName(kind), rawValue(kind).replace(QLatin1Char('&'), QStringLiteral("&&")) + (isEditable(kind) ? QStringLiteral("…") : QString()), QKeySequence(),
|
|
[this, kind] { session().addAdjustment(kind); });
|
|
add(layer, QStringLiteral("editAdjustment"), QStringLiteral("Edit Adjustment…"), QKeySequence(),
|
|
[this] { session().setAdjustmentEditingID(session().activeLayerID()); });
|
|
layer->addSeparator();
|
|
// Swift's transformCommand: with selected pixels, Transform Selection.
|
|
add(layer, QStringLiteral("transformLayer"), QStringLiteral("Transform Layer"), QKeySequence(Qt::CTRL | Qt::Key_T), [this] { session().transformCommand(); });
|
|
// Swift's layerViaCopy: a selection makes it Layer via Copy.
|
|
add(layer, QStringLiteral("layerViaCopy"), QStringLiteral("Duplicate Layer"), QKeySequence(Qt::CTRL | Qt::Key_J), [this] { session().layerViaCopy(); });
|
|
layer->addSeparator();
|
|
add(layer, QStringLiteral("clippingMask"), QStringLiteral("Create Clipping Mask"), QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_G), [this] {
|
|
if (const std::optional<QUuid> id = session().activeLayerID())
|
|
session().toggleClippingMask(*id);
|
|
});
|
|
layer->addSeparator();
|
|
add(layer, QStringLiteral("groupLayers"), QStringLiteral("Group Selected Layers"), QKeySequence(Qt::CTRL | Qt::Key_G), [this] { session().groupSelectedLayers(); });
|
|
add(layer, QStringLiteral("ungroupLayers"), QStringLiteral("Ungroup Layers"), QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_G), [this] { session().ungroupLayers(); });
|
|
add(layer, QStringLiteral("moveOutOfFolder"), QStringLiteral("Move Out of Folder"), QKeySequence(), [this] { session().moveActiveLayerOutOfGroup(); });
|
|
add(layer, QStringLiteral("newBlankLayer"), QStringLiteral("New Blank Layer"), QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_N), [this] { session().addBlankLayer(); });
|
|
add(layer, QStringLiteral("renameLayer"), QStringLiteral("Rename Layer…"), QKeySequence(), [this] { session().setRenamingLayerID(session().activeLayerID()); });
|
|
add(layer, QStringLiteral("layerVisibility"), QStringLiteral("Hide Layer"), QKeySequence(), [this] {
|
|
if (const std::optional<QUuid> id = session().activeLayerID())
|
|
session().toggleLayerVisibility(*id);
|
|
});
|
|
layer->addSeparator();
|
|
add(layer, QStringLiteral("moveLayerUp"), QStringLiteral("Move Layer Up"), QKeySequence(Qt::CTRL | Qt::Key_BracketRight), [this] { session().moveActiveLayer(1); });
|
|
add(layer, QStringLiteral("moveLayerDown"), QStringLiteral("Move Layer Down"), QKeySequence(Qt::CTRL | Qt::Key_BracketLeft), [this] { session().moveActiveLayer(-1); });
|
|
add(layer, QStringLiteral("mergeLayers"), QStringLiteral("Merge Down"), QKeySequence(Qt::CTRL | Qt::Key_E), [this] { session().mergeLayers(); });
|
|
layer->addSeparator();
|
|
add(layer, QStringLiteral("flipHorizontal"), QStringLiteral("Flip Layer Horizontal"), QKeySequence(), [this] { session().flipLayers(true); });
|
|
add(layer, QStringLiteral("flipVertical"), QStringLiteral("Flip Layer Vertical"), QKeySequence(), [this] { session().flipLayers(false); });
|
|
layer->addSeparator();
|
|
add(layer, QStringLiteral("deleteLayer"), QStringLiteral("Delete Layer"), QKeySequence(), [this] { session().deleteLayerOrMask(); });
|
|
|
|
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();
|
|
}
|
|
|
|
void CompositorMenus::listRecent(QMenu *recent)
|
|
{
|
|
recent->clear();
|
|
for (const QString &path : RecentProjects::shared().paths())
|
|
// A name's ampersand is literal, no mnemonic.
|
|
connect(recent->addAction(ProjectTab::nameWithoutSuffix(path).replace(QLatin1Char('&'), QStringLiteral("&&"))), &QAction::triggered, this, [this, path] { projects().open(path); });
|
|
recent->addSeparator();
|
|
QAction *clear = recent->addAction(QStringLiteral("Clear Menu"));
|
|
clear->setObjectName(QStringLiteral("clearRecent"));
|
|
clear->setEnabled(!RecentProjects::shared().paths().isEmpty());
|
|
connect(clear, &QAction::triggered, this, [] { RecentProjects::shared().clear(); });
|
|
}
|
|
|
|
QAction *CompositorMenus::add(QMenu *menu, const QString &name, const QString &text, const QKeySequence &shortcut, const std::function<void()> &run)
|
|
{
|
|
QAction *made = menu->addAction(text);
|
|
made->setObjectName(name);
|
|
made->setProperty("originalShortcut", shortcut);
|
|
made->setShortcut(ShortcutSettings::shared().menu(shortcut));
|
|
// Keys still work while Canvas Only hides the bar.
|
|
m_window.addAction(made);
|
|
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);
|
|
}
|
|
|
|
void CompositorMenus::watchFront()
|
|
{
|
|
// The front tab's session drives every entry.
|
|
if (m_watchedTab != std::optional(m_workspace.current().id)) {
|
|
m_watchedTab = m_workspace.current().id;
|
|
disconnect(m_sessionWatch);
|
|
m_sessionWatch = connect(&session(), &EditorSession::changed, this, &CompositorMenus::synchronize);
|
|
}
|
|
synchronize();
|
|
}
|
|
|
|
void CompositorMenus::focusMoved(QWidget *, QWidget *to)
|
|
{
|
|
// The menu bar borrows focus; the field keeps Undo.
|
|
if (qobject_cast<QMenuBar *>(to) == nullptr) {
|
|
m_field = qobject_cast<QLineEdit *>(to);
|
|
m_canvas = qobject_cast<CanvasView *>(to);
|
|
}
|
|
synchronize();
|
|
}
|
|
|
|
void CompositorMenus::synchronize()
|
|
{
|
|
EditorSession &s = session();
|
|
ProjectController &p = projects();
|
|
const std::optional<ImageLayer> active = s.activeLayer();
|
|
const bool drawn = s.document().has_value();
|
|
// Swift's text branch: open text makes the entries bare.
|
|
const bool typing = m_field != nullptr || s.textDraft().has_value();
|
|
action(QStringLiteral("newCanvas"))->setEnabled(p.canStart());
|
|
action(QStringLiteral("openProject"))->setEnabled(p.canStart());
|
|
action(QStringLiteral("openRecent"))->setEnabled(p.canStart());
|
|
action(QStringLiteral("importImages"))->setEnabled(!s.levels() && !s.showsBusy() && !s.isImporting() && !s.showsNewDocument());
|
|
action(QStringLiteral("save"))->setEnabled(drawn && p.canStart());
|
|
action(QStringLiteral("saveAs"))->setEnabled(drawn && p.canStart());
|
|
action(QStringLiteral("exportPNG"))->setEnabled(drawn && p.canStart());
|
|
action(QStringLiteral("exportJPEG"))->setEnabled(drawn && p.canStart());
|
|
action(QStringLiteral("exportAs"))->setEnabled(drawn && p.canStart());
|
|
action(QStringLiteral("closeProject"))->setEnabled(p.canStart());
|
|
// A step's ampersand, doubled: a lone one is a mnemonic.
|
|
const auto shown = [](QString name) { return name.replace(QLatin1Char('&'), QStringLiteral("&&")); };
|
|
// In a field, Undo is the field's and bare.
|
|
action(QStringLiteral("undo"))->setText(!typing && s.canUndo() ? QStringLiteral("Undo %1").arg(shown(s.history.undoName())) : QStringLiteral("Undo"));
|
|
action(QStringLiteral("undo"))->setEnabled(typing || s.canUndo());
|
|
action(QStringLiteral("redo"))->setText(!typing && s.canRedo() ? QStringLiteral("Redo %1").arg(shown(s.history.redoName())) : QStringLiteral("Redo"));
|
|
action(QStringLiteral("redo"))->setEnabled(typing || s.canRedo());
|
|
for (const char *name : {"fit", "actualPixels", "zoomIn", "zoomOut"})
|
|
action(QString::fromLatin1(name))->setEnabled(drawn);
|
|
action(QStringLiteral("navigator"))->setChecked(NavigatorMinimap::visibleSetting());
|
|
action(QStringLiteral("pixelGrid"))->setChecked(s.showsPixelGrid());
|
|
action(QStringLiteral("toggleFullscreen"))->setChecked(s.canvasOnly());
|
|
action(QStringLiteral("toggleFullscreen"))->setEnabled(s.canToggleCanvasOnly());
|
|
const bool opened = s.document().has_value();
|
|
for (const auto &[name, value] : std::initializer_list<std::pair<const char *, bool>>{
|
|
{"showGrid", s.showsGrid()}, {"showGuides", s.showsGuides()}, {"showRulers", s.showsRulers()}, {"snapEnabled", s.snapEnabled()},
|
|
{"snapToGuides", s.snapToGuides()}, {"snapToGrid", s.snapToGrid()}, {"snapToLayers", s.snapToLayers()},
|
|
{"snapToDocumentBounds", s.snapToDocumentBounds()}, {"lockGuides", s.locksGuides()}}) {
|
|
action(QString::fromLatin1(name))->setChecked(value);
|
|
action(QString::fromLatin1(name))->setEnabled(opened);
|
|
}
|
|
action(QStringLiteral("clearGuides"))->setEnabled(s.canClearGuides());
|
|
action(QStringLiteral("gridSettings"))->setEnabled(opened);
|
|
action(QStringLiteral("transformControls"))->setChecked(s.showsTransformControls());
|
|
action(QStringLiteral("transformControls"))->setEnabled(s.tool() == NavigationTool::move && s.document().has_value());
|
|
const bool selected = s.selection().has_value();
|
|
action(QStringLiteral("deselect"))->setEnabled(selected && s.canEditSelection());
|
|
action(QStringLiteral("inverse"))->setEnabled(selected && s.canEditSelection());
|
|
action(QStringLiteral("layerPixels"))->setEnabled(active && active->asset && s.canEditSelection());
|
|
action(QStringLiteral("maskBlackAreas"))->setEnabled(active && active->mask && s.canEditSelection());
|
|
action(QStringLiteral("subject"))->setEnabled(s.canSelectSubject());
|
|
action(QStringLiteral("colorRange"))->setEnabled(s.canSelectColorRange());
|
|
for (const char *name : {"expandSelection", "contractSelection", "featherSelection"})
|
|
action(QString::fromLatin1(name))->setEnabled(s.canModifySelection());
|
|
action(QStringLiteral("levels"))->setEnabled(s.canAdjustColors() && !s.hueSaturation());
|
|
action(QStringLiteral("lastFilter"))->setText(s.lastFilter() ? QStringLiteral("Last Filter: ") + rawValue(s.lastFilter().value()) : QStringLiteral("Last Filter"));
|
|
action(QStringLiteral("lastFilter"))->setEnabled(s.canRepeatLastFilter());
|
|
for (const FilterKind kind : allFilterKinds) {
|
|
if (kind != FilterKind::contentAwareFill)
|
|
action(filterName(kind))->setEnabled((kind == FilterKind::vignette ? s.canVignette() : s.canAdjustColors()) && !s.hueSaturation());
|
|
}
|
|
action(QStringLiteral("hueSaturation"))->setEnabled(s.canAdjustColors());
|
|
action(QStringLiteral("invert"))->setText(s.isMaskSelected() ? QStringLiteral("Invert Mask") : QStringLiteral("Invert"));
|
|
action(QStringLiteral("invert"))->setEnabled(s.canInvert());
|
|
action(QStringLiteral("canvasSize"))->setEnabled(drawn && p.canStart());
|
|
action(QStringLiteral("imageSize"))->setEnabled(drawn && p.canStart());
|
|
action(QStringLiteral("trim"))->setEnabled(drawn && p.canStart());
|
|
action(QStringLiteral("rotateCanvasClockwise"))->setEnabled(s.canEditLayers());
|
|
action(QStringLiteral("rotateCanvasCounterclockwise"))->setEnabled(s.canEditLayers());
|
|
action(QStringLiteral("flipCanvasHorizontal"))->setEnabled(s.canEditLayers());
|
|
action(QStringLiteral("flipCanvasVertical"))->setEnabled(s.canEditLayers());
|
|
action(QStringLiteral("newAdjustmentLayer"))->setEnabled(s.canEditLayers());
|
|
// Beyond Swift: Invert has no editor, so nothing to open.
|
|
action(QStringLiteral("editAdjustment"))->setEnabled(s.canEditLayers() && active && active->adjustment && isEditable(active->adjustment->kind));
|
|
action(QStringLiteral("transformLayer"))->setText(s.canTransformSelection() ? QStringLiteral("Transform Selection") : QStringLiteral("Transform Layer"));
|
|
action(QStringLiteral("transformLayer"))->setEnabled(s.canTransform() || s.canTransformSelection());
|
|
action(QStringLiteral("clippingMask"))->setText(active && active->maskSourceID ? QStringLiteral("Release Clipping Mask") : QStringLiteral("Create Clipping Mask"));
|
|
action(QStringLiteral("clippingMask"))->setEnabled(s.activeLayerID() && s.canToggleClippingMask(*s.activeLayerID()));
|
|
action(QStringLiteral("groupLayers"))->setEnabled(s.canEditLayers());
|
|
action(QStringLiteral("ungroupLayers"))->setEnabled(s.canUngroupLayers());
|
|
action(QStringLiteral("moveOutOfFolder"))->setEnabled(s.canEditLayers() && active && active->parentID);
|
|
action(QStringLiteral("newBlankLayer"))->setEnabled(s.canEditLayers());
|
|
action(QStringLiteral("renameLayer"))->setEnabled(s.canEditLayers() && active);
|
|
action(QStringLiteral("layerVisibility"))->setText(active && !active->isVisible ? QStringLiteral("Show Layer") : QStringLiteral("Hide Layer"));
|
|
action(QStringLiteral("layerVisibility"))->setEnabled(s.canEditLayers() && active);
|
|
action(QStringLiteral("moveLayerUp"))->setEnabled(s.canMoveActiveLayer(1));
|
|
action(QStringLiteral("moveLayerDown"))->setEnabled(s.canMoveActiveLayer(-1));
|
|
action(QStringLiteral("copyMerged"))->setEnabled(s.canCopyMerged());
|
|
action(QStringLiteral("contentAwareFill"))->setEnabled(s.canContentAwareFill());
|
|
action(QStringLiteral("fillForeground"))->setEnabled(s.canEditPixels());
|
|
action(QStringLiteral("fillBackground"))->setEnabled(s.canEditPixels());
|
|
action(QStringLiteral("clearSelectionPixels"))->setEnabled(s.selection().has_value() && s.canEditPixels());
|
|
action(QStringLiteral("layerViaCopy"))->setText(s.selection() ? QStringLiteral("Layer via Copy") : QStringLiteral("Duplicate Layer"));
|
|
action(QStringLiteral("layerViaCopy"))->setEnabled(s.canCopyPixels() || (!s.selection() && s.canEditLayers() && active));
|
|
action(QStringLiteral("mergeLayers"))->setText(s.mergeTitle());
|
|
action(QStringLiteral("mergeLayers"))->setEnabled(s.canMergeLayers());
|
|
action(QStringLiteral("flipHorizontal"))->setEnabled(s.canTransform());
|
|
action(QStringLiteral("flipVertical"))->setEnabled(s.canTransform());
|
|
const std::optional<LayerEffectSelection> effect = s.selectedEffect();
|
|
action(QStringLiteral("deleteLayer"))->setText(effect ? QStringLiteral("Delete ") + rawValue(effect->kind)
|
|
: s.isMaskSelected() && active && active->mask ? QStringLiteral("Delete Layer Mask")
|
|
: s.selectedLayerIDs().size() > 1 ? QStringLiteral("Delete Layers")
|
|
: QStringLiteral("Delete Layer"));
|
|
action(QStringLiteral("deleteLayer"))->setEnabled(s.canEditLayers() && active);
|
|
}
|