From ec2995ad127c3cda9437bb16fa7feb55904b4093 Mon Sep 17 00:00:00 2001 From: sspaeti Date: Fri, 21 Aug 2026 23:23:01 +0200 Subject: [PATCH] feat(editor): Save As dialog on Ctrl+Shift+S Adds a Save As action that opens a file dialog to pick a destination, writes a copy there, and keeps the editor open. The chosen folder is remembered for the next Save As. Default outputs (Enter/Ctrl+C/Ctrl+S) are unchanged. --- README.md | 1 + src/capture.cpp | 21 +++++++++++---- src/capture.hpp | 4 +++ src/editor.cpp | 71 +++++++++++++++++++++++++++++++++++++++++++++++++ src/editor.hpp | 1 + 5 files changed, 93 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index be65c893..7368ccfa 100644 --- a/README.md +++ b/README.md @@ -286,6 +286,7 @@ Install the corresponding Tesseract language data before adding a language to | `Ctrl+Shift+Z`, `Ctrl+Y` | Redo | | `Ctrl+C` | Copy PNG only | | `Ctrl+S` | Save PNG only | +| `Ctrl+Shift+S` | Save As: pick a destination in a file dialog; the editor stays open, and the chosen folder is remembered for the next Save As | | `Enter` | Copy and save | | `P` | Pin the capture on screen and close the editor | | `Esc` | Return to Select; press again to close | diff --git a/src/capture.cpp b/src/capture.cpp index 9f5ba25c..ffdee27f 100644 --- a/src/capture.cpp +++ b/src/capture.cpp @@ -168,11 +168,7 @@ QString runtimePath(const QString &name) { } QString screenshotTargetPath(QString &error) { - QString root = qEnvironmentVariable("OMASNAP_SCREENSHOT_DIR"); - if (root.isEmpty()) - root = - QDir(QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)) - .filePath(QStringLiteral("Screenshots")); + const QString root = screenshotRootDir(); if (!QDir().mkpath(root)) { error = QStringLiteral("Could not create screenshot directory: %1").arg(root); @@ -1044,6 +1040,21 @@ bool quickOutput(const QImage &image, QuickOutputMode mode, QString &error) { return true; } +QString screenshotRootDir() { + QString root = qEnvironmentVariable("OMASNAP_SCREENSHOT_DIR"); + if (root.isEmpty()) + root = + QDir(QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)) + .filePath(QStringLiteral("Screenshots")); + return root; +} + +QString defaultScreenshotFileName() { + return QStringLiteral("screenshot-%1.png") + .arg(QDateTime::currentDateTime().toString( + QStringLiteral("yyyy-MM-dd_HH-mm-ss"))); +} + QString moveSnapshotToScreenshots(const QString &sourcePath, QString &error) { const QString targetPath = screenshotTargetPath(error); if (targetPath.isEmpty()) diff --git a/src/capture.hpp b/src/capture.hpp index 3520b866..c56b46ba 100644 --- a/src/capture.hpp +++ b/src/capture.hpp @@ -190,6 +190,10 @@ QImage applyRedactionsScaled(QImage image, const QVector &redactions [[nodiscard]] bool ensurePrivateDirectory(const QString &path); /** Returns Omasnap's private runtime directory, or empty on failure. */ [[nodiscard]] QString secureRuntimeDirectory(); +/** Returns the screenshot output directory without creating it. */ +[[nodiscard]] QString screenshotRootDir(); +/** Returns a timestamped `screenshot-….png` file name. */ +[[nodiscard]] QString defaultScreenshotFileName(); [[nodiscard]] QString moveSnapshotToScreenshots(const QString &sourcePath, QString &error); [[nodiscard]] QString temporarySnapshotPath(); diff --git a/src/editor.cpp b/src/editor.cpp index 53579cd1..73260981 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -2614,6 +2616,68 @@ void CaptureEditor::finish(OutputMode mode) { close(); } +void CaptureEditor::saveAs() { + // Ctrl+Shift+S: pick a destination, write a copy there, and keep the + // editor open so Enter/Ctrl+C/Ctrl+S still produce the default outputs. + if (busy_ || selection_.isEmpty()) + return; + busy_ = true; + setStatus(QStringLiteral("Preparing screenshot…")); + snapshotOutputRequested_ = true; + scheduleSnapshot(); + const bool snapshotOk = waitForSnapshot(); + // Later mid-edit writes go back to the fast crash-recovery compression. + snapshotOutputRequested_ = false; + const QFileInfo snapshotFile(snapshotPath_); + if (!snapshotOk || snapshotPath_.isEmpty() || !snapshotFile.exists() || + snapshotFile.size() <= 0) { + busy_ = false; + setStatus(QStringLiteral("Could not prepare screenshot snapshot")); + return; + } + + QSettings settings(defaultPaletteConfigPath(), QSettings::IniFormat); + QString startDir = + settings.value(QStringLiteral("output/save_as_dir")).toString(); + if (startDir.isEmpty() || !QDir(startDir).exists()) + startDir = screenshotRootDir(); + const QString suggested = + QDir(startDir).filePath(defaultScreenshotFileName()); + // The editor is a fullscreen stays-on-top surface, so a dialog opened on + // top of it ends up buried behind it with no way to reach either window. + // Hide the overlay for the dialog's lifetime (parent nullptr keeps the + // dialog mapped while the editor is hidden), then restore it. + hide(); + QString target = QFileDialog::getSaveFileName( + nullptr, QStringLiteral("Save screenshot as"), suggested, + QStringLiteral("PNG image (*.png)")); + show(); + raise(); + activateWindow(); + if (target.isEmpty()) { + busy_ = false; + setStatus(QStringLiteral("Save As cancelled")); + return; + } + if (!target.endsWith(QStringLiteral(".png"), Qt::CaseInsensitive)) + target += QStringLiteral(".png"); + if (QFile::exists(target) && !QFile::remove(target)) { + busy_ = false; + setStatus(QStringLiteral("Could not overwrite: %1").arg(target)); + return; + } + if (!QFile::copy(snapshotPath_, target)) { + busy_ = false; + setStatus(QStringLiteral("Could not save to: %1").arg(target)); + return; + } + settings.setValue(QStringLiteral("output/save_as_dir"), + QFileInfo(target).absolutePath()); + busy_ = false; + setStatus(QStringLiteral("Saved to %1").arg(target)); + sendCaptureNotification(QStringLiteral("Screenshot saved"), target); +} + void CaptureEditor::handleToolbar(const QString &action) { const Tool toolBefore = tool_; const QString statusBefore = status_; @@ -2860,6 +2924,12 @@ void CaptureEditor::keyPressEvent(QKeyEvent *event) { } else if (event->matches(QKeySequence::Copy)) { finish(OutputMode::Copy); return; + } else if (event->matches(QKeySequence::SaveAs) || + (event->key() == Qt::Key_S && + event->modifiers().testFlag(Qt::ControlModifier) && + event->modifiers().testFlag(Qt::ShiftModifier))) { + saveAs(); + return; } else if (event->matches(QKeySequence::Save)) { finish(OutputMode::Save); return; @@ -4663,6 +4733,7 @@ void CaptureEditor::paintEdit(QPainter &painter) { {QStringLiteral("Enter"), QStringLiteral("Copy + save")}, {QStringLiteral("Ctrl+C"), QStringLiteral("Copy only")}, {QStringLiteral("Ctrl+S"), QStringLiteral("Save only")}, + {QStringLiteral("Ctrl+Shift+S"), QStringLiteral("Save As…")}, {QStringLiteral("Esc"), QStringLiteral("Arrow / twice close")}}, keepVisible); if (hoveredButton) { diff --git a/src/editor.hpp b/src/editor.hpp index 0df5526e..9ebc0051 100644 --- a/src/editor.hpp +++ b/src/editor.hpp @@ -314,6 +314,7 @@ class CaptureEditor final : public QWidget { void redoEdit(); void selectWindowInDirection(int key); void finish(OutputMode mode); + void saveAs(); void handleEscape(); void handleToolbar(const QString &action); void paintEdit(QPainter &painter);