#include "UI/CompositorMenus.h" #include "IO/RecentProjects.h" #include "Logging.h" #include "UI/KeyboardShortcuts.h" #include "UI/ProjectWorkspaceView.h" #include #include 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("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 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 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 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 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 &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()) { const QVariant original = entry->property("originalShortcut"); if (original.isValid()) entry->setShortcut(ShortcutSettings::shared().menu(original.value())); } } QAction *CompositorMenus::action(const QString &name) const { return parent()->findChild(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(to) == nullptr) { m_field = qobject_cast(to); m_canvas = qobject_cast(to); } synchronize(); } void CompositorMenus::synchronize() { EditorSession &s = session(); ProjectController &p = projects(); const std::optional 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("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>{ {"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 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); }