diff --git a/README.md b/README.md index 68f1edec..7fdc892a 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,9 @@ resizable vector layers and preserves the monitor's native pixels on scaled disp ## Features -- Freeform region, window, and full-monitor capture modes. +- Smart selection by default: drag a freeform region, click a window to crop + it, or click open monitor space for the full monitor. Explicit region, + window, fullscreen, and scrolling-region modes remain available. - A pointer-side readout that turns any drag into a ruler: the pointer position while the crosshair is idle, then the frame size in native export pixels while a region, a hovered window, or a crop handle is being sized. @@ -160,7 +162,9 @@ omasnap --help ## CLI capture modes -Running without arguments opens freeform region selection: +Running without arguments opens smart selection. Drag for a freeform region, +click a window to capture it, or click outside every window to capture the +focused monitor: ```bash omasnap @@ -192,7 +196,7 @@ Compatibility positional names are also accepted: omasnap region omasnap windows omasnap fullscreen -omasnap smart # maps to region selection +omasnap smart ``` These options choose what is initially selected; the editor still controls whether the @@ -325,28 +329,25 @@ Install the corresponding Tesseract language data before adding a language to ### Capture selection -Tabs across the top of the overlay switch the capture kind: **Region**, -**Window**, **Scrolling Region**, **Fullscreen**. All four are modes of the -same overlay. Scrolling Region selects exactly like Region; once the region is -drawn, the page inside it goes live and the scroll controls appear in place. -Region and Scrolling Region frame the same rectangle, so switching between the -two keeps it: the frame drawn for a scrolling capture is captured as a region, -and a region just captured frames the scroll panel. Window and Fullscreen pick -an area of their own, so switching to either starts over. -The tabs stay up in the editor too: a tab there drops the edit and goes back to -capturing in that mode, and a small **Scroll capture** button under the image -turns the drawn region into a scrolling capture. The keys below do the same -without reaching for the pointer. +The default smart picker infers the capture kind from the gesture: drag for a +region, click a window for that window, or click open space for the full +focused monitor. Whatever is lit is what will be captured. + +Press `S` before drawing to select a scrolling region; once drawn, the page +inside it goes live and the scroll controls appear in place. A small **Scroll +capture** button under an image already open in the editor turns that region +into a scrolling capture. Explicit `region`, `windows`, `fullscreen`, and +`scroll` command-line targets remain available for scripts and keybindings. | Input | Action | |---|---| +| Click | In smart mode, capture the window under the pointer, or the full monitor outside any window | | Drag | Select a region, with its native pixel size shown at the pointer | -| `Space` | Step through the capture-kind tabs (Region, Window, Scrolling Region) | | `S` | Toggle scrolling-region mode | | `R` | Restore the last region drawn this session (same monitor) | | `SUPER + Arrow` | Move among windows in window mode | | `Enter` | Capture the highlighted window | -| `Ctrl+A` | Select the full focused monitor (the Fullscreen tab) | +| `Ctrl+A` | Select the full focused monitor | | Hover the right-edge stack | Fan out the five most recent captures; click one to reopen it | | `Esc` | Dismiss (while selecting; in the editor, `Esc` returns to Select and a second `Esc` closes) | @@ -437,7 +438,7 @@ edges. After the canvas grows, those crop handles remain on the original source make check ``` -The smoke executable exercises region/window/fullscreen startup modes, capture selection, +The smoke executable exercises smart/region/window/fullscreen startup modes, capture selection, working-document persistence (source plus op-log JSON), annotation tools, undo/redo replay, vector movement and scaling, text editing, OCR, native-DPI output, endpoint-only line selection, annotation-driven canvas growth and clipping policies, diff --git a/src/editor.cpp b/src/editor.cpp index 1c0a9aaf..325a8441 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -73,8 +73,8 @@ constexpr std::array kTextSizeNames{"S", "M", "L"}; constexpr qreal kToolbarWidth = 840; // Toolbar row to the top of the image below it. constexpr qreal kToolbarImageGap = 18.0; -// Tab strip's bottom edge to the toolbar row above it. -constexpr qreal kTabToolbarGap = 8.0; +// Preserve the editor's established top inset without capture-mode chrome. +constexpr qreal kToolbarTop = 39.0; /// Extra spacing between toolbar groups (history / style / tools / actions), /// so the row reads as clusters rather than one flat strip. Ordinary gaps are /// tightened from 4px to 2.5px; across the 20 gaps that exactly pays for these @@ -788,9 +788,14 @@ CaptureEditor::CaptureEditor(CaptureData capture, CaptureMode mode, cuts_.clear(); redactionBaseStale_ = true; switch (pendingMode_) { + case CaptureMode::Smart: + smartMode_ = true; + hoveredWindow_ = windowAt(cursor_); + setStatus({}); + break; case CaptureMode::Fullscreen: selection_ = QRectF(QPointF(), capture_.previewSize); - editedKind_ = SelectTab::Fullscreen; + editedMode_ = CaptureMode::Fullscreen; enterSelectedCapture(QStringLiteral( "Full screen selected · native resolution · outer handles " "crop")); @@ -799,12 +804,10 @@ CaptureEditor::CaptureEditor(CaptureData capture, CaptureMode mode, windowMode_ = true; hoveredWindow_ = windowAt(cursor_); setStatus(QStringLiteral( - "Window mode · click or Super+Arrows then Enter · Space " - "returns to area")); + "Window mode · click or Super+Arrows then Enter")); break; case CaptureMode::Region: - setStatus(QStringLiteral( - "Drag to select an area · Space selects a window")); + setStatus(QStringLiteral("Drag to select an area")); break; case CaptureMode::Scroll: scrollMode_ = true; @@ -841,6 +844,7 @@ CaptureEditor::CaptureEditor(CaptureData capture, CaptureMode mode, }); captureMode_ = mode; + smartMode_ = mode == CaptureMode::Smart; liveMonitor_ = capture_.monitor; if (capture_.source.isNull()) { // The pixel capture has not landed yet; the overlay shows a Capturing… @@ -849,7 +853,7 @@ CaptureEditor::CaptureEditor(CaptureData capture, CaptureMode mode, pendingMode_ = mode; setStatus(QStringLiteral("Capturing screen…")); } else if (mode == CaptureMode::Fullscreen || mode == CaptureMode::File) { - editedKind_ = SelectTab::Fullscreen; + editedMode_ = CaptureMode::Fullscreen; if (ops_.isEmpty()) selection_ = QRectF(QPointF(), capture_.previewSize); else @@ -875,11 +879,14 @@ CaptureEditor::CaptureEditor(CaptureData capture, CaptureMode mode, enterEdit(editStatus); else enterSelectedCapture(editStatus); + } else if (mode == CaptureMode::Smart) { + hoveredWindow_ = windowAt(cursor_); + setStatus({}); } else if (mode == CaptureMode::Window) { windowMode_ = true; hoveredWindow_ = windowAt(cursor_); - setStatus(QStringLiteral("Window mode · click or Super+Arrows then Enter · " - "Space selects a scrolling region")); + setStatus(QStringLiteral( + "Window mode · click or Super+Arrows then Enter")); } else if (mode == CaptureMode::Scroll) { scrollMode_ = true; setStatus(QStringLiteral( @@ -1796,9 +1803,7 @@ QRectF CaptureEditor::normalizedSelection(const QPointF &first, } qreal CaptureEditor::toolbarTop() const { - // Just under the tab strip's fixed bottom edge — independent of the image, - // so the two can never overlap regardless of window size or image shape. - return kCaptureTabBarBottom + kTabToolbarGap; + return kToolbarTop; } qreal CaptureEditor::imageTopMargin() const { @@ -2124,7 +2129,7 @@ QString CaptureEditor::measurementText() const { if (phase_ == Phase::Select) { if (recentsOpen_) return {}; - if (windowMode_) { + if (windowMode_ || (smartMode_ && !dragging_ && hoveredWindow_ >= 0)) { if (hoveredWindow_ < 0 || hoveredWindow_ >= capture_.windows.size()) return {}; return formatPixelSize( @@ -3011,11 +3016,15 @@ void CaptureEditor::handleEscape() { } dragging_ = false; selection_ = {}; - setStatus(windowMode_ - ? QStringLiteral("Window mode · click or Super+Arrows then " - "Enter · Space selects a scrolling region") - : QStringLiteral( - "Drag to select an area · Space selects a window")); + if (smartMode_) + hoveredWindow_ = windowAt(cursor_); + if (smartMode_) + setStatus({}); + else if (windowMode_) + setStatus( + QStringLiteral("Window mode · click or Super+Arrows then Enter")); + else + setStatus(QStringLiteral("Drag to select an area")); updatePointerCursor(); update(); return; @@ -3060,8 +3069,9 @@ void CaptureEditor::chooseWindow(int index) { return; selection_ = QRectF(capture_.windows.at(index).rect); redactionBaseStale_ = true; + smartMode_ = false; windowMode_ = false; - editedKind_ = SelectTab::Window; + editedMode_ = CaptureMode::Window; enterSelectedCapture(QStringLiteral( "Window selected · Select moves layers · Ctrl+wheel zooms · outer handles " "crop")); @@ -3673,16 +3683,20 @@ void CaptureEditor::keyPressEvent(QKeyEvent *event) { const bool directionalKey = event->key() == Qt::Key_Left || event->key() == Qt::Key_Right || event->key() == Qt::Key_Up || event->key() == Qt::Key_Down; - if (windowMode_ && directionalKey && + if ((windowMode_ || smartMode_) && directionalKey && event->modifiers().testFlag(Qt::MetaModifier)) { selectWindowInDirection(event->key()); event->accept(); update(); return; } - if (windowMode_ && + if ((windowMode_ || smartMode_) && (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)) { - chooseWindow(hoveredWindow_); + if (smartMode_ && (hoveredWindow_ < 0 || + event->modifiers().testFlag(Qt::ControlModifier))) + selectFullscreen(); + else + chooseWindow(hoveredWindow_); return; } if (!windowMode_ && !dragging_ && event->key() == Qt::Key_R && @@ -3711,27 +3725,6 @@ void CaptureEditor::keyPressEvent(QKeyEvent *event) { setScrollMode(!scrollMode_); return; } - if (event->key() == Qt::Key_Space) { - // Space steps along the tab strip. Fullscreen is skipped: it captures - // on the spot, and a cycle key that fires it on the way past would be - // a trap rather than a mode. - const QVector tabs = selectTabItems(); - int current = -1; - for (int index = 0; index < tabs.size(); ++index) { - if (tabs.at(index).kind == selectKind()) { - current = index; - break; - } - } - for (int step = 1; step <= tabs.size(); ++step) { - const SelectTab next = tabs.at((current + step) % tabs.size()).kind; - if (next != SelectTab::Fullscreen) { - activateSelectTab(next); - break; - } - } - return; - } QWidget::keyPressEvent(event); return; } @@ -4021,13 +4014,6 @@ QRegion CaptureEditor::pointerMotionRegion(const QPointF &point) const { // and still tiny beside a 6K surface. add(QRectF(point.x() - 230, point.y() - 70, 460, 140)); - for (const CaptureTab &tab : selectTabItems()) { - if (tab.rect.contains(point)) { - add(tab.rect.adjusted(-4, -4, 4, 4)); - break; - } - } - if (phase_ == Phase::Select) { if (!windowMode_ && !dragging_ && !recentsOpen_) { add(QRectF(point.x() - 3, 0, 7, height())); @@ -4178,6 +4164,34 @@ QRegion CaptureEditor::pointerMotionRegion(const QPointF &point) const { return damage; } +QRegion CaptureEditor::windowHoverDamage(int oldIndex, int newIndex) const { + const auto isWindow = [this](int index) { + return index >= 0 && index < capture_.windows.size(); + }; + const bool oldIsWindow = isWindow(oldIndex); + const bool newIsWindow = isWindow(newIndex); + + // Smart mode changes the meaning of every backdrop pixel when it crosses + // between a window and fullscreen, so that transition needs one full paint. + if (smartMode_ && oldIsWindow != newIsWindow) + return QRegion(rect()); + + QRegion damage; + const auto addWindow = [this, &damage](int index) { + if (index < 0 || index >= capture_.windows.size()) + return; + // The antialiased 2 px outline straddles the target rectangle. Include + // its outer pixels so the backing store cannot leave stale dark segments. + damage |= QRegion( + mapPreviewToWidget(QRectF(capture_.windows.at(index).rect)) + .adjusted(-3, -3, 3, 3) + .toAlignedRect()); + }; + addWindow(oldIndex); + addWindow(newIndex); + return damage & QRegion(rect()); +} + void CaptureEditor::queuePointerRepaint(const QRegion &damage) { pendingPointerDamage_ |= damage & QRegion(rect()); if (!pendingPointerDamage_.isEmpty() && !pointerRepaintTimer_.isActive()) @@ -4201,7 +4215,12 @@ void CaptureEditor::mouseMoveEvent(QMouseEvent *event) { if (phase_ == Phase::Select) { if (!dragging_) trackRecentsHover(); - if (windowMode_) + if (smartMode_) { + if (dragging_) + selection_ = normalizedSelection(dragStart_, cursor_); + else + hoveredWindow_ = recentsOpen_ ? -1 : windowAt(cursor_); + } else if (windowMode_) hoveredWindow_ = recentsOpen_ ? -1 : windowAt(cursor_); else if (dragging_) selection_ = normalizedSelection(dragStart_, cursor_); @@ -4486,16 +4505,9 @@ void CaptureEditor::mouseMoveEvent(QMouseEvent *event) { const QRegion newHole(selection_.normalized().toAlignedRect()); damage |= oldHole.xored(newHole); } - if (phase_ == Phase::Select && windowMode_ && + if (phase_ == Phase::Select && (windowMode_ || smartMode_) && oldHoveredWindow != hoveredWindow_) { - if (oldHoveredWindow >= 0 && oldHoveredWindow < capture_.windows.size()) - damage |= QRegion(mapPreviewToWidget( - QRectF(capture_.windows.at(oldHoveredWindow).rect)) - .toAlignedRect()); - if (hoveredWindow_ >= 0 && hoveredWindow_ < capture_.windows.size()) - damage |= QRegion(mapPreviewToWidget( - QRectF(capture_.windows.at(hoveredWindow_).rect)) - .toAlignedRect()); + damage |= windowHoverDamage(oldHoveredWindow, hoveredWindow_); } queuePointerRepaint(damage); } @@ -4524,6 +4536,8 @@ void CaptureEditor::mousePressEvent(QMouseEvent *event) { if (dragging_) { dragging_ = false; selection_ = {}; + if (smartMode_) + hoveredWindow_ = windowAt(cursor_); update(); } } else if (phase_ == Phase::Edit) { @@ -4554,17 +4568,11 @@ void CaptureEditor::mousePressEvent(QMouseEvent *event) { return; cursor_ = event->position(); endNudgeRun(); - if (const int tab = selectTabAt(cursor_); tab >= 0) { - if (phase_ == Phase::Edit && textEditing()) - acceptText(); - activateSelectTab(selectTabItems().at(tab).kind); - return; - } if (phase_ == Phase::Edit && scrollPillRect().contains(cursor_)) { if (textEditing()) acceptText(); const QRect region = selection_.toRect(); - returnToSelect(false); + returnToSelect(); setScrollMode(true); startScrollCapture(region); return; @@ -4583,6 +4591,13 @@ void CaptureEditor::mousePressEvent(QMouseEvent *event) { dragStart_ = cursor_; selection_ = {}; dragging_ = true; + if (smartMode_) { + hoveredWindow_ = -1; + // Starting a drag clears the inferred window/fullscreen target. This is + // an infrequent semantic transition, so repaint once before pointer + // motion returns to narrow damage regions. + update(); + } return; } if (customColorPickerOpen_) { @@ -4888,6 +4903,19 @@ void CaptureEditor::mouseReleaseEvent(QMouseEvent *event) { if (phase_ == Phase::Select) { selection_ = normalizedSelection(dragStart_, event->position()); dragging_ = false; + const qreal selectedArea = selection_.width() * selection_.height(); + if (smartMode_ && (selectedArea < 20.0 || selection_.width() < 2.0 || + selection_.height() < 2.0)) { + selection_ = {}; + const int window = windowAt(event->position()); + if (window >= 0) + chooseWindow(window); + else + selectFullscreen(); + updatePointerCursor(); + update(); + return; + } if (selection_.width() >= 2 && selection_.height() >= 2) { // Remember the drawn region for this session, so R can bring it back // on the next capture. A convenience, so failing to write is no error. @@ -5316,14 +5344,13 @@ void CaptureEditor::updatePointerCursor() { } if (phase_ == Phase::Select) { clearHighlighterPreview(); - applyCursor(windowMode_ || selectTabAt(cursor_) >= 0 || - (recentsOpen_ && recentAt(cursor_) >= 0) - ? Qt::PointingHandCursor - : recentsOpen_ ? Qt::ArrowCursor - : Qt::CrossCursor); + const bool pointing = + windowMode_ || (recentsOpen_ && recentAt(cursor_) >= 0); + applyCursor(pointing ? Qt::PointingHandCursor + : recentsOpen_ ? Qt::ArrowCursor : Qt::CrossCursor); return; } - if (selectTabAt(cursor_) >= 0 || scrollPillRect().contains(cursor_)) { + if (scrollPillRect().contains(cursor_)) { clearHighlighterPreview(); applyCursor(Qt::PointingHandCursor); return; @@ -5458,69 +5485,12 @@ void CaptureEditor::refreshBackdropCache() { } } -QVector CaptureEditor::selectTabItems() const { - // In the edit phase the strip stays as the way back: a tab there returns - // to the select phase in that mode. A file has no screen to go back to. - if (capture_.source.isNull() || (phase_ == Phase::Edit && !hasLiveScreen())) - return {}; - return captureTabLayout(rect()); -} - -int CaptureEditor::selectTabAt(const QPointF &position) const { - return captureTabAt(selectTabItems(), position); -} - -void CaptureEditor::activateSelectTab(SelectTab tab) { - // A frame drawn for a scrolling capture is the same rectangle a region - // capture wants, so it comes along to Region rather than being drawn a - // second time. Window and Fullscreen pick an area of their own, so there it - // is dropped. - QRect scrolled; - if (scrollPanel_) { - scrolled = scrollPanel_->region(); - endScrollCapture(); - } - const bool fromEdit = phase_ == Phase::Edit; - const QRect edited = selection_.toRect(); - if (fromEdit) - returnToSelect(tab == SelectTab::Window); - switch (tab) { - case SelectTab::Region: - setScrollMode(false); - setWindowMode(false); - if (!scrolled.isEmpty()) - commitRegion(QRectF(scrolled), - QStringLiteral("Area selected · Select moves layers · wheel " - "zooms · outer handles crop")); - break; - case SelectTab::Scroll: - setScrollMode(true); - // From the editor the drawn region goes along, so the scroll frame - // starts where this capture was. - if (fromEdit) - startScrollCapture(edited); - break; - case SelectTab::Window: - setScrollMode(false); - setWindowMode(true); - break; - case SelectTab::Fullscreen: - selectFullscreen(); - break; - } -} - -CaptureKind CaptureEditor::selectKind() const { - return windowMode_ ? CaptureKind::Window - : scrollMode_ ? CaptureKind::Scroll - : CaptureKind::Region; -} - bool CaptureEditor::hasLiveScreen() const { return captureMode_ != CaptureMode::File && !liveMonitor_.name.isEmpty(); } void CaptureEditor::setScrollMode(bool enabled) { + smartMode_ = false; scrollMode_ = enabled; if (enabled) windowMode_ = false; @@ -5529,20 +5499,20 @@ void CaptureEditor::setScrollMode(bool enabled) { hoveredWindow_ = -1; setStatus(enabled ? QStringLiteral("Drag to select a scrolling region · the " "page inside stays live") - : QStringLiteral( - "Drag to select an area · Space selects a window")); + : QStringLiteral("Drag to select an area")); updatePointerCursor(); update(); } void CaptureEditor::commitRegion(const QRectF ®ion, const QString &editStatus) { + smartMode_ = false; selection_ = region; if (scrollMode_) { startScrollCapture(region.toRect()); return; } - editedKind_ = SelectTab::Region; + editedMode_ = CaptureMode::Region; enterSelectedCapture(editStatus); } @@ -5565,8 +5535,6 @@ void CaptureEditor::startScrollCapture(const QRect ®ion) { endScrollCapture(); setScrollMode(true); }); - connect(panel, &ScrollCapturePanel::tabRequested, this, - [this](CaptureKind kind) { activateSelectTab(kind); }); panel->show(); panel->raise(); panel->setFocus(Qt::OtherFocusReason); @@ -5598,7 +5566,7 @@ void CaptureEditor::adoptStitched(const QImage &image) { } const bool veryLong = image.width() > stitch::kWidelyOpenableEdge || image.height() > stitch::kWidelyOpenableEdge; - adoptImage(image, OperationLog(), SelectTab::Scroll, + adoptImage(image, OperationLog(), CaptureMode::Scroll, veryLong ? QStringLiteral("Very long capture (%1 × %2) · edits and " "saves here as usual, but many apps cannot " @@ -5611,13 +5579,12 @@ void CaptureEditor::adoptStitched(const QImage &image) { "crop")); } -void CaptureEditor::adoptImage(QImage image, OperationLog log, SelectTab kind, +void CaptureEditor::adoptImage(QImage image, OperationLog log, CaptureMode kind, const QString &status) { // The editor normally works on a region of the frozen screen. Here it is // handed an image instead (a stitched scroll, a shelved capture, a file) // and edits that: the image is the whole capture, at the scale its log was - // written in, and the screen stays known by name so the tabs can capture - // it again. + // written in, while retaining the live monitor identity for scroll capture. if (scrollPanel_) endScrollCapture(); if (textEditing()) { @@ -5642,10 +5609,11 @@ void CaptureEditor::adoptImage(QImage image, OperationLog log, SelectTab kind, redactionBaseStale_ = true; backdropKey_ = 0; scrollMode_ = false; + smartMode_ = false; windowMode_ = false; hoveredWindow_ = -1; handedImage_ = true; - editedKind_ = kind; + editedMode_ = kind; selectedAnnotation_ = -1; selectedAnnotations_.clear(); if (ops_.isEmpty()) @@ -5658,7 +5626,7 @@ void CaptureEditor::adoptImage(QImage image, OperationLog log, SelectTab kind, enterSelectedCapture(status); } -void CaptureEditor::returnToSelect(bool windowMode) { +void CaptureEditor::returnToSelect() { if (textEditing()) { textEditor_->clear(); textEditor_->hide(); @@ -5687,22 +5655,19 @@ void CaptureEditor::returnToSelect(bool windowMode) { capture_.monitor = liveMonitor_; pristineSource_ = {}; captureStarted_ = false; - startCapture(windowMode ? CaptureMode::Window : CaptureMode::Region, true); + startCapture(CaptureMode::Region, true); } phase_ = Phase::Select; tool_ = Tool::Select; viewZoom_ = 1.0; viewOffset_ = {}; selection_ = {}; - windowMode_ = windowMode; - hoveredWindow_ = windowMode_ ? windowAt(cursor_) : -1; + smartMode_ = false; + windowMode_ = false; + hoveredWindow_ = -1; redactionBaseStale_ = true; scheduleSnapshot(); - setStatus(windowMode_ - ? QStringLiteral("Window mode · click or Super+Arrows then " - "Enter · Space selects a scrolling region") - : QStringLiteral( - "Drag to select an area · Space selects a window")); + setStatus(QStringLiteral("Drag to select an area")); updatePointerCursor(); update(); } @@ -5729,22 +5694,6 @@ QRectF CaptureEditor::scrollPillRect() const { return QRectF(image.center().x() - width / 2.0, y, width, height); } -void CaptureEditor::setWindowMode(bool enabled) { - windowMode_ = enabled; - if (enabled) - scrollMode_ = false; - dragging_ = false; - selection_ = {}; - hoveredWindow_ = windowMode_ ? windowAt(cursor_) : -1; - setStatus(windowMode_ - ? QStringLiteral("Window mode · click or Super+Arrows then " - "Enter · Space selects a scrolling region") - : QStringLiteral( - "Drag to select an area · Space selects a window")); - updatePointerCursor(); - update(); -} - void CaptureEditor::loadRecents() { recentsLoading_ = true; recentsWatcher_.setFuture( @@ -5998,29 +5947,22 @@ void CaptureEditor::completeReopenRecent(const ReopenResult &result) { return; } editingRecent_ = result.recent; - adoptImage(result.image, result.log, SelectTab::Region, + adoptImage(result.image, result.log, CaptureMode::Region, QStringLiteral("Reopened recent capture · Copy/Save to output")); } void CaptureEditor::selectFullscreen() { + smartMode_ = false; windowMode_ = false; dragging_ = false; hoveredWindow_ = -1; selection_ = QRectF(QPointF(), capture_.previewSize); - editedKind_ = SelectTab::Fullscreen; + editedMode_ = CaptureMode::Fullscreen; enterSelectedCapture(QStringLiteral( "Full screen selected · native resolution · outer handles crop")); update(); } -void CaptureEditor::paintSelectTabs(QPainter &painter) { - // In the select phase the lit one is the mode the pointer is in; in the - // edit phase it is how this capture was taken. - const CaptureKind active = - phase_ == Phase::Edit ? editedKind_ : selectKind(); - drawCaptureTabs(painter, selectTabItems(), active, cursor_); -} - void CaptureEditor::paintSelect(QPainter &painter) { if (capture_.source.isNull()) { painter.fillRect(rect(), QColor(0, 0, 0, kBackdropDim)); @@ -6033,28 +5975,47 @@ void CaptureEditor::paintSelect(QPainter &painter) { painter.drawPixmap(rect(), dimmedBackdrop_); } const bool exporting = phase_ == Phase::Export; - // Drawn first, low-opacity, no card: the live/frozen screen, the tabs, the - // selection all paint over it wherever they overlap. - if (!exporting) - drawHotkeyLegend(painter, rect(), - {{QStringLiteral("Drag"), QStringLiteral("Area")}, - {QStringLiteral("Space"), QStringLiteral("Window")}, - {QStringLiteral("Ctrl+A"), QStringLiteral("Fullscreen")}, - {QStringLiteral("R"), QStringLiteral("Last region")}, - {QStringLiteral("S"), QStringLiteral("Scrolling region")}, - {QStringLiteral("Esc"), QStringLiteral("Close")}}); - + // Drawn first, low-opacity, no card: the live/frozen screen and selection + // paint over it wherever they overlap. + if (!exporting) { + QVector> hotkeys; + if (smartMode_) + hotkeys = { + {QStringLiteral("Click"), QStringLiteral("Window / full screen")}, + {QStringLiteral("Drag"), QStringLiteral("Area")}, + {QStringLiteral("R"), QStringLiteral("Last region")}, + {QStringLiteral("S"), QStringLiteral("Scrolling region")}, + {QStringLiteral("Esc"), QStringLiteral("Close")}}; + else + hotkeys = {{QStringLiteral("Drag"), QStringLiteral("Area")}, + {QStringLiteral("Ctrl+A"), QStringLiteral("Fullscreen")}, + {QStringLiteral("R"), QStringLiteral("Last region")}, + {QStringLiteral("S"), QStringLiteral("Scrolling region")}, + {QStringLiteral("Esc"), QStringLiteral("Close")}}; + drawHotkeyLegend(painter, rect(), hotkeys); + } + + const bool smartWindow = smartMode_ && !dragging_ && hoveredWindow_ >= 0 && + hoveredWindow_ < capture_.windows.size(); + const bool smartFullscreen = + smartMode_ && !dragging_ && !recentsOpen_ && hoveredWindow_ < 0; + const bool targetWindow = windowMode_ || smartWindow; const bool haveHole = exporting ? !selection_.isEmpty() - : windowMode_ ? hoveredWindow_ >= 0 && hoveredWindow_ < capture_.windows.size() - : !selection_.isEmpty(); + : targetWindow + ? hoveredWindow_ >= 0 && hoveredWindow_ < capture_.windows.size() + : smartFullscreen || !selection_.isEmpty(); if (haveHole) { const bool previewCoordinates = - windowMode_ || (exporting && editedKind_ != SelectTab::Region); - const QRectF previewHole = - windowMode_ ? QRectF(capture_.windows.at(hoveredWindow_).rect) - : previewCoordinates ? selection_ - : mapWidgetToPreview(selection_); + targetWindow || smartFullscreen || + (exporting && editedMode_ != CaptureMode::Region); + QRectF previewHole = selection_; + if (targetWindow) + previewHole = QRectF(capture_.windows.at(hoveredWindow_).rect); + else if (smartFullscreen) + previewHole = QRectF(QPointF(), capture_.previewSize); + else if (!previewCoordinates) + previewHole = mapWidgetToPreview(selection_); const QRectF destHole = previewCoordinates ? mapPreviewToWidget(previewHole) : selection_; @@ -6072,8 +6033,13 @@ void CaptureEditor::paintSelect(QPainter &painter) { painter.setBrush(Qt::NoBrush); painter.drawRect(mapPreviewToWidget(QRectF(window.rect))); } + } else if (smartWindow) { + painter.setPen(QPen(Qt::white, 2)); + painter.setBrush(Qt::NoBrush); + painter.drawRect( + mapPreviewToWidget(QRectF(capture_.windows.at(hoveredWindow_).rect))); } else if (!selection_.isEmpty()) { - const QRectF outline = exporting && editedKind_ != SelectTab::Region + const QRectF outline = exporting && editedMode_ != CaptureMode::Region ? mapPreviewToWidget(selection_) : selection_; painter.setPen(QPen(Qt::white, 2)); @@ -6086,10 +6052,8 @@ void CaptureEditor::paintSelect(QPainter &painter) { painter.drawLine(QPointF(cursor_.x(), 0), QPointF(cursor_.x(), height())); painter.drawLine(QPointF(0, cursor_.y()), QPointF(width(), cursor_.y())); } - if (!exporting) { + if (!exporting) paintRecents(painter); - paintSelectTabs(painter); - } drawStatusPill(painter, rect(), status_); if (!exporting) drawMeasureBadge(painter, rect(), cursor_, measurementText()); @@ -6671,7 +6635,6 @@ void CaptureEditor::paintEdit(QPainter &painter) { kTextSizeNames.at(static_cast(index)))); } } - paintSelectTabs(painter); if (const QRectF pill = scrollPillRect(); !pill.isNull()) { // A way into scroll capture from a region already drawn: the scroll // overlay opens with this frame in place. diff --git a/src/editor.hpp b/src/editor.hpp index 9f18fb1d..b658712d 100644 --- a/src/editor.hpp +++ b/src/editor.hpp @@ -44,7 +44,7 @@ class Window; class CaptureEditor final : public QWidget { Q_OBJECT public: - enum class CaptureMode { Region, Scroll, Window, Fullscreen, File }; + enum class CaptureMode { Smart, Region, Scroll, Window, Fullscreen, File }; explicit CaptureEditor(CaptureData capture, CaptureMode mode = CaptureMode::Region, @@ -283,6 +283,9 @@ class CaptureEditor final : public QWidget { [[nodiscard]] bool scrollPanelActiveForTest() const { return scrollPanel_ != nullptr; } + [[nodiscard]] QRectF scrollPillRectForTest() const { + return scrollPillRect(); + } /// Blocks until the shelf has been listed; false when it is empty. bool waitForRecents(); /// Whether the shelf is fanned out. Test accessor. @@ -308,6 +311,10 @@ class CaptureEditor final : public QWidget { [[nodiscard]] QRegion pointerMotionRegionForTest(const QPointF &point) const { return pointerMotionRegion(point); } + [[nodiscard]] QRegion windowHoverDamageForTest(int oldIndex, + int newIndex) const { + return windowHoverDamage(oldIndex, newIndex); + } /// Whether the selection chrome is currently stepped back for an adjustment. /// Test accessor. [[nodiscard]] bool selectionFadedForTest() const { @@ -322,6 +329,8 @@ class CaptureEditor final : public QWidget { [[nodiscard]] bool shapeMenuOpenForTest() const { return shapeMenuOpen_; } /// Whether window selection is active in the select phase. Test accessor. [[nodiscard]] bool windowModeForTest() const { return windowMode_; } + /// Whether clicks are inferred as window/fullscreen while drags stay areas. + [[nodiscard]] bool smartModeForTest() const { return smartMode_; } /// Where the image is drawn on screen right now (widget pixels), and the /// annotation-space-to-widget scale. Test accessor: lets a test compute /// exact click/expectation points from real geometry instead of hand math. @@ -364,13 +373,6 @@ class CaptureEditor final : public QWidget { [[nodiscard]] bool exportingForTest() const { return phase_ == Phase::Export; } /// Whether the annotation editor is visible. Test accessor. [[nodiscard]] bool editingForTest() const { return phase_ == Phase::Edit; } - /// Widget rect of the capture-kind tab with `label`, or null. Test accessor. - [[nodiscard]] QRectF selectTabRectForTest(const QString &label) const { - for (const CaptureTab &item : selectTabItems()) - if (captureTabLabel(item.kind) == label) - return item.rect; - return {}; - } [[nodiscard]] bool textSizeMenuOpenForTest() const { return textSizeMenuOpen_; } private: @@ -407,11 +409,10 @@ class CaptureEditor final : public QWidget { [[nodiscard]] int cropHandleAt(const QPointF &point) const; /// Fit-to-window rect for the selection (unaffected by the view zoom/pan). [[nodiscard]] QRectF baseImageRect() const; - /// Top of the toolbar row: just under the tab strip's fixed bottom edge, - /// independent of the image, so the two can never overlap. + /// Top of the toolbar row, inset from the surface edge. [[nodiscard]] qreal toolbarTop() const; - /// How much vertical room the tab strip and toolbar actually need, at the - /// current window width — the image's top margin, not a guessed constant. + /// How much vertical room the toolbar actually needs at the current window + /// width — the image's top margin, not a guessed constant. [[nodiscard]] qreal imageTopMargin() const; /// baseImageRect transformed by the current view zoom and pan (content and /// annotations map through this). Equals baseImageRect at zoom 1. @@ -468,18 +469,11 @@ class CaptureEditor final : public QWidget { void beginText(const QPointF &point, int annotationIndex = -1, int lineCapacity = 1); void chooseWindow(int index); - /// Capture-kind tabs across the top of the select overlay. Region and - /// Window are modes (one is always lit); Fullscreen acts at once. - using SelectTab = CaptureKind; - [[nodiscard]] QVector selectTabItems() const; - [[nodiscard]] int selectTabAt(const QPointF &position) const; - void activateSelectTab(SelectTab tab); - void setWindowMode(bool enabled); void setScrollMode(bool enabled); void selectFullscreen(); /// Back from the editor to the select phase: the op log is dropped and the /// frozen screen is offered again for a new region or window. - void returnToSelect(bool windowMode); + void returnToSelect(); /// Scroll capture takes over the surface with `region` drawn. void startScrollCapture(const QRect ®ion); /// Tears the scroll panel down; the surface is whole again. @@ -488,8 +482,8 @@ class CaptureEditor final : public QWidget { void adoptStitched(const QImage &image); /// The editor's other mode of working: not a region of the frozen screen /// but an image handed to it, with the op log it was last edited with. - /// `kind` is the tab lit for it. - void adoptImage(QImage image, OperationLog log, SelectTab kind, + /// `kind` records which coordinate space produced it. + void adoptImage(QImage image, OperationLog log, CaptureMode kind, const QString &status); /// Leaves the select phase with a drawn region: edit it, or scroll it. void commitRegion(const QRectF ®ion, const QString &editStatus); @@ -499,7 +493,6 @@ class CaptureEditor final : public QWidget { /// Small pill under the image in the edit phase offering scroll capture of /// the drawn region; null when not offered. [[nodiscard]] QRectF scrollPillRect() const; - void paintSelectTabs(QPainter &painter); /// The shelf of earlier captures along the right edge of the select /// overlay: a stack of small cards that fans out under the pointer, each /// reopening its capture in place of taking a new one. @@ -562,6 +555,7 @@ class CaptureEditor final : public QWidget { void paintOcrOverlay(QPainter &painter, const QRectF &image, qreal scale); void setStatus(QString status); [[nodiscard]] QRegion pointerMotionRegion(const QPointF &point) const; + [[nodiscard]] QRegion windowHoverDamage(int oldIndex, int newIndex) const; void queuePointerRepaint(const QRegion &damage); void toggleShapeFill(); void toggleTextBackground(); @@ -603,12 +597,11 @@ class CaptureEditor final : public QWidget { /// The monitor as captured, kept apart from capture_.monitor (which a /// stitched result replaces) so the screen can be captured again. MonitorInfo liveMonitor_; - [[nodiscard]] CaptureKind selectKind() const; LayerShellQt::Window *layer_ = nullptr; ScrollCapturePanel *scrollPanel_ = nullptr; CaptureMode captureMode_ = CaptureMode::Region; - /// Which tab produced the capture being edited; lit in the edit phase. - SelectTab editedKind_ = SelectTab::Region; + /// Coordinate space used by the capture being edited. + CaptureMode editedMode_ = CaptureMode::Region; std::optional editingRecent_; QVector recents_; QFutureWatcher> recentsWatcher_; @@ -672,6 +665,9 @@ class CaptureEditor final : public QWidget { qreal cutBandHi_ = 0.0; qreal cutDragRatio_ = 1.0; qreal cutDragOriginOffset_ = 0.0; + /// The default picker: a drag is a region; a click is the window under the + /// pointer, or the whole monitor when no window is there. + bool smartMode_ = false; bool windowMode_ = false; BackgroundStyle backgroundStyle_ = BackgroundStyle::None; bool imageShadow_ = true; @@ -776,8 +772,7 @@ class CaptureEditor final : public QWidget { QString snapshotPath_; QuickOutputMode quickOutputMode_ = QuickOutputMode::None; int pinCount_ = 0; - QString status_ = - QStringLiteral("Drag to select an area · Space selects a window"); + QString status_ = QStringLiteral("Drag to select an area"); InlineTextEdit *textEditor_ = nullptr; QPointF textPoint_; QVector originalSelectedAnnotations_; diff --git a/src/main.cpp b/src/main.cpp index ea146887..d9a641db 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -130,6 +130,9 @@ int main(int argc, char **argv) { "Native Wayland screenshot and annotation overlay for Hyprland and " "Omarchy.\n" "\n" + "With no target, drag for a region, click a window, or click open " + "space for\nthe full focused monitor.\n" + "\n" "Only one capture overlay runs at a time. Starting omasnap again while " "an\noverlay is open dismisses it: the running instance is asked to " "quit and the\nnew process exits without capturing, so the same hotkey " @@ -150,7 +153,7 @@ int main(int argc, char **argv) { QStringLiteral("Start in window selection mode.")); const QCommandLineOption regionOption( QStringLiteral("capture-region"), - QStringLiteral("Start in freeform region selection mode (default).")); + QStringLiteral("Start in freeform region selection mode.")); parser.addOption(fullscreenOption); parser.addOption(windowOption); parser.addOption(regionOption); @@ -185,8 +188,8 @@ int main(int argc, char **argv) { parser.addOption(scrollOption); parser.addPositionalArgument( QStringLiteral("target"), - QStringLiteral("Capture mode (smart, region, windows, fullscreen) or the " - "path of an image file to edit."), + QStringLiteral("Capture mode (smart, region, windows, fullscreen, " + "scroll) or the path of an image file to edit."), QStringLiteral("[target]")); parser.process(application); startupTimingMark("command line parsed"); @@ -202,7 +205,7 @@ int main(int argc, char **argv) { else if (parser.isSet(saveOption)) quickOutputMode = QuickOutputMode::Save; - CaptureEditor::CaptureMode captureMode = CaptureEditor::CaptureMode::Region; + CaptureEditor::CaptureMode captureMode = CaptureEditor::CaptureMode::Smart; int requestedModes = parser.isSet(fullscreenOption) + parser.isSet(windowOption) + parser.isSet(regionOption) + parser.isSet(scrollOption); @@ -242,8 +245,9 @@ int main(int argc, char **argv) { else if (mode == QStringLiteral("windows") || mode == QStringLiteral("window")) captureMode = CaptureEditor::CaptureMode::Window; - else if (mode == QStringLiteral("smart") || - mode == QStringLiteral("region")) + else if (mode == QStringLiteral("smart")) + captureMode = CaptureEditor::CaptureMode::Smart; + else if (mode == QStringLiteral("region")) captureMode = CaptureEditor::CaptureMode::Region; else if (mode == QStringLiteral("scroll")) captureMode = CaptureEditor::CaptureMode::Scroll; diff --git a/src/overlay-chrome.cpp b/src/overlay-chrome.cpp index 466e2b17..3b5aa026 100644 --- a/src/overlay-chrome.cpp +++ b/src/overlay-chrome.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include @@ -42,106 +41,6 @@ QFont chromeMonoFont(int pixelSize, bool bold) { return font; } -QString captureTabLabel(CaptureKind kind) { - switch (kind) { - case CaptureKind::Region: - return QStringLiteral("REGION"); - case CaptureKind::Scroll: - return QStringLiteral("SCROLLING REGION"); - case CaptureKind::Window: - return QStringLiteral("WINDOW"); - case CaptureKind::Fullscreen: - return QStringLiteral("FULLSCREEN"); - } - return {}; -} - -namespace { -QFont captureTabFont() { - QFont font(QStringLiteral("Noto Sans")); - font.setBold(true); - font.setPixelSize(11); - return font; -} -QColor captureTabAccent(CaptureKind kind) { - switch (kind) { - case CaptureKind::Window: - return QColor(QStringLiteral("#ffd60a")); - case CaptureKind::Fullscreen: - return QColor(QStringLiteral("#0a84ff")); - case CaptureKind::Region: - case CaptureKind::Scroll: - break; - } - return QColor(QStringLiteral("#30d158")); -} -} // namespace - -QVector captureTabLayout(const QRect &bounds) { - static const CaptureKind order[] = {CaptureKind::Region, CaptureKind::Window, - CaptureKind::Scroll, - CaptureKind::Fullscreen}; - const QFontMetricsF metrics(captureTabFont()); - constexpr qreal kPad = 14.0; - constexpr qreal kGap = 2.0; - constexpr qreal kHeight = 26.0; - // Flush to the top edge (see drawCaptureTabs' -30 background extension): - // derived from kCaptureTabBarBottom rather than a separate magic number, - // so the two can't drift apart. - constexpr qreal kTop = kCaptureTabBarBottom - kHeight - 5.0; - QVector tabs; - qreal total = 0.0; - for (const CaptureKind kind : order) { - const qreal w = metrics.horizontalAdvance(captureTabLabel(kind)) + 2 * kPad; - tabs.push_back({kind, QRectF(total, kTop, w, kHeight)}); - total += w + kGap; - } - total -= kGap; - const qreal left = bounds.left() + (bounds.width() - total) / 2.0; - for (CaptureTab &tab : tabs) - tab.rect.translate(left, 0); - return tabs; -} - -int captureTabAt(const QVector &tabs, const QPointF &position) { - for (int index = 0; index < tabs.size(); ++index) { - if (tabs.at(index).rect.adjusted(-2, -6, 2, 6).contains(position)) - return index; - } - return -1; -} - -void drawCaptureTabs(QPainter &painter, const QVector &tabs, - CaptureKind active, const QPointF &cursor) { - if (tabs.isEmpty()) - return; - // Hangs off the top edge like a tab strip: square at the top (drawn past - // the edge so only the bottom corners round), not a floating pill. - const QRectF bar = tabs.constFirst().rect.united(tabs.constLast().rect) - .adjusted(-5, -30, 5, 5); - painter.setPen(QPen(QColor(255, 255, 255, 32), 1)); - painter.setBrush(QColor(18, 18, 22, 235)); - painter.drawRoundedRect(bar, 12, 12); - painter.setFont(captureTabFont()); - const int hovered = captureTabAt(tabs, cursor); - for (int index = 0; index < tabs.size(); ++index) { - const CaptureTab &tab = tabs.at(index); - painter.setPen(Qt::NoPen); - if (tab.kind == active) { - painter.setBrush(captureTabAccent(tab.kind)); - painter.drawRoundedRect(tab.rect, 9, 9); - painter.setPen(QColor(18, 18, 22)); - } else { - if (index == hovered) { - painter.setBrush(QColor(255, 255, 255, 28)); - painter.drawRoundedRect(tab.rect, 9, 9); - } - painter.setPen(QColor(255, 255, 255, index == hovered ? 255 : 190)); - } - painter.drawText(tab.rect, Qt::AlignCenter, captureTabLabel(tab.kind)); - } -} - QRectF drawModeBadge(QPainter &painter, const QRect &bounds, const QString &label, const QColor &accent, QRectF *closeRect) { @@ -204,6 +103,9 @@ void drawHotkeyLegend(QPainter &painter, const QRect &bounds, void drawStatusPill(QPainter &painter, const QRect &bounds, const QString &text) { + if (text.isEmpty()) + return; + QFont font(QStringLiteral("Noto Sans")); font.setPixelSize(13); painter.setFont(font); diff --git a/src/overlay-chrome.hpp b/src/overlay-chrome.hpp index e4a8cb82..e65e2845 100644 --- a/src/overlay-chrome.hpp +++ b/src/overlay-chrome.hpp @@ -1,13 +1,10 @@ -/** @fileoverview The chrome every full-screen overlay wears: the mode badge at - * the top, the hotkey guide in the corner, and the status pill along the - * bottom. Capture and scroll capture are the same tool in two moods, so they - * are drawn by the same code rather than by two that drift apart. */ +/** @fileoverview Shared overlay chrome: pinned fonts, the mode badge, hotkey + * guide, and status pill. */ #pragma once #include #include #include -#include #include #include #include @@ -19,7 +16,7 @@ class QPainter; /// tooltips): pinned in code, never the platform theme's system font, so /// startup does not depend on a theme plugin and the look is the same on /// every install. Adwaita Sans is the stock Omarchy UI font; Noto Sans is -/// the fallback the tab strip already uses. +/// the bundled fallback. [[nodiscard]] QFont chromeFont(int pixelSize, bool bold = false); /// The application-wide default font, installed by main() before any widget /// exists: the same face and 11 pt size the gtk3 platform theme used to @@ -32,29 +29,6 @@ class QPainter; /// (on Omarchy, the face `omarchy-font-set` chose). [[nodiscard]] QFont chromeMonoFont(int pixelSize, bool bold = false); -/// The kinds of capture the tab strip across the top offers, on every -/// overlay. Region and Window are modes of the area overlay, Scroll is the -/// scroll overlay, and Fullscreen acts at once. -enum class CaptureKind { Region, Scroll, Window, Fullscreen }; -struct CaptureTab { - CaptureKind kind; - QRectF rect; -}; -/// Visible height of the tab strip's background, from the top edge (the -/// strip is flush against it) to its rounded bottom — fixed regardless of -/// window size, since only the horizontal layout changes with the surface. -/// Chrome stacked below the strip anchors to this, not a guessed constant. -constexpr qreal kCaptureTabBarBottom = 31.0; -[[nodiscard]] QString captureTabLabel(CaptureKind kind); -/// Tab positions for a surface of `bounds`, hanging off the top edge. -[[nodiscard]] QVector captureTabLayout(const QRect &bounds); -/// Index of the tab under `position`, or -1. -[[nodiscard]] int captureTabAt(const QVector &tabs, - const QPointF &position); -/// Draws the strip; `active` is lit, the tab under `cursor` is hinted. -void drawCaptureTabs(QPainter &painter, const QVector &tabs, - CaptureKind active, const QPointF &cursor); - /// The badge naming what the overlay is doing, centered at the top, with the × /// that leaves it. Returns the whole badge; `closeRect` is the × alone, for /// hit-testing the click that closes. @@ -66,7 +40,7 @@ QRectF drawModeBadge(QPainter &painter, const QRect &bounds, /// left, growing upward, in low-opacity text. Hotkeys are a reference, not /// UI: there is no card, no border, and no attempt to dodge the pointer or /// dodge anything else — draw it early (right after the overlay's initial -/// dim fill, before the image, the tab strip, the toolbar, any popup) and +/// dim fill, before the image, toolbar, or any popup) and /// normal paint order does the rest, since whatever is drawn afterward /// simply covers it wherever the two overlap. void drawHotkeyLegend(QPainter &painter, const QRect &bounds, diff --git a/src/scroll-capture.cpp b/src/scroll-capture.cpp index 7528b0c2..f1ea9f9c 100644 --- a/src/scroll-capture.cpp +++ b/src/scroll-capture.cpp @@ -257,8 +257,6 @@ void ScrollCapturePanel::postStatus(const QString &status, bool warning) { QVector ScrollCapturePanel::chromeRects() const { QVector rects; - for (const CaptureTab &tab : captureTabLayout(rect())) - rects.push_back(tab.rect.toAlignedRect()); if (phase_ == Phase::Selected) { for (int index = 0; index < kModeButtonCount; ++index) rects.push_back(modeButtonRect(index)); @@ -1012,7 +1010,7 @@ void ScrollCapturePanel::paintEvent(QPaintEvent *) { painter.fillRect(region_, Qt::transparent); painter.setCompositionMode(QPainter::CompositionMode_SourceOver); // Drawn first, low-opacity, no card: the live page, the region outline, - // the pills and the tab strip all paint over it wherever they overlap. + // the pills paint over it wherever they overlap. drawHotkeyLegend(painter, rect(), legendEntries()); painter.setPen(QPen(statusWarning_ ? kWarn : kAccent, 2)); painter.setBrush(Qt::NoBrush); @@ -1098,10 +1096,6 @@ void ScrollCapturePanel::paintEvent(QPaintEvent *) { } } } - // The same tab strip every overlay wears, with this kind lit. The other - // tabs leave for the area overlay in that mode. - drawCaptureTabs(painter, captureTabLayout(rect()), CaptureKind::Scroll, - cursor_); drawStatusPill(painter, rect(), status_); } @@ -1144,16 +1138,6 @@ void ScrollCapturePanel::mousePressEvent(QMouseEvent *event) { } if (event->button() != Qt::LeftButton) return; - if (const int tab = captureTabAt(captureTabLayout(rect()), event->position()); - tab >= 0) { - const CaptureKind kind = captureTabLayout(rect()).at(tab).kind; - if (kind != CaptureKind::Scroll) { - stopWorker(); - phase_ = Phase::Finished; - emit tabRequested(kind); - } - return; - } if (phase_ == Phase::Capturing) { const QPoint point = event->position().toPoint(); if (doneButtonRect().contains(point)) @@ -1205,8 +1189,6 @@ void ScrollCapturePanel::mousePressEvent(QMouseEvent *event) { void ScrollCapturePanel::mouseMoveEvent(QMouseEvent *event) { const QPoint point = event->position().toPoint(); - cursor_ = point; - update(); // the tab strip's hover hint follows the pointer updateKeyboardZone(point); if (activeGrip_ != Grip::None) { // The button is down, so motion keeps arriving even over the hole. diff --git a/src/scroll-capture.hpp b/src/scroll-capture.hpp index d02224cc..d9761ade 100644 --- a/src/scroll-capture.hpp +++ b/src/scroll-capture.hpp @@ -78,7 +78,7 @@ class Window; /// drawn (the editor's own region selection is the selection): choose manual /// or automatic, scroll, or let the injection worker scroll, and stitch. It /// manages the layer's input hole and keyboard grab while it is up, and -/// hands back with stitched(), dismissed() or tabRequested(). +/// hands back with stitched() or dismissed(). class ScrollCapturePanel final : public QWidget { Q_OBJECT public: @@ -89,24 +89,17 @@ class ScrollCapturePanel final : public QWidget { QWidget *parent); ~ScrollCapturePanel() override; /// Take over with `region` (logical surface pixels, clamped to the - /// surface and the chrome strip) already drawn. Call once, after show(). + /// surface and the controls) already drawn. Call once, after show(). void begin(const QRect ®ion); /// Stops any worker, hands the surface back whole (no hole, keyboard /// exclusive again) and hides. Safe to call from inside one of this /// panel's own signals; the destructor calls it too. void release(); - /// The frame as it stands, in logical surface pixels. Empty until one has - /// been drawn. The editor reads it when another tab is picked, so the - /// rectangle carries across instead of having to be drawn again. - [[nodiscard]] QRect region() const { return region_; } - signals: /// The capture is done: `image` is the stitched result. void stitched(const QImage &image); /// Cancelled, or a fresh region was asked for: back to selecting. void dismissed(); - /// A tab on the strip other than Scrolling Region was clicked. - void tabRequested(CaptureKind kind); protected: void paintEvent(QPaintEvent *event) override; @@ -204,8 +197,6 @@ class ScrollCapturePanel final : public QWidget { MonitorInfo monitor_; LayerShellQt::Window *layer_ = nullptr; Phase phase_ = Phase::Selected; - /// Last pointer position, for the tab strip's hover hint. - QPoint cursor_; QRect region_; // logical widget pixels, normalized Mode mode_ = Mode::Manual; stitch::Axis axis_ = stitch::Axis::Vertical; diff --git a/tests/editor-smoke.cpp b/tests/editor-smoke.cpp index 02eb6f78..47ad63e9 100644 --- a/tests/editor-smoke.cpp +++ b/tests/editor-smoke.cpp @@ -315,9 +315,8 @@ bool runSelectUndimHoleCheck(QString &error) { CaptureData capture = selectHoleCapture( preview, sourceSize, 1.0, QRectF(windowRect), {{{windowRect}, QStringLiteral("w1"), QStringLiteral("one")}}); - CaptureEditor editor(capture); + CaptureEditor editor(capture, CaptureEditor::CaptureMode::Window); prepareSelectEditor(editor, widget); - QTest::keyClick(&editor, Qt::Key_Space); // Region -> Window QTest::mouseMove(&editor, QPoint(100, 90), 20); QApplication::processEvents(); const QImage ui = editor.grab().toImage(); @@ -336,9 +335,8 @@ bool runSelectUndimHoleCheck(QString &error) { CaptureData capture = selectHoleCapture( preview, sourceSize, 1.0, QRectF(windowRect), {{{windowRect}, QStringLiteral("w1"), QStringLiteral("rotated")}}); - CaptureEditor editor(capture); + CaptureEditor editor(capture, CaptureEditor::CaptureMode::Window); prepareSelectEditor(editor, widget); - QTest::keyClick(&editor, Qt::Key_Space); // Region -> Window QTest::mouseMove(&editor, QPoint(320, 180), 20); QApplication::processEvents(); const QImage ui = editor.grab().toImage(); @@ -393,9 +391,8 @@ bool runSelectUndimHoleCheck(QString &error) { CaptureData capture = selectHoleCapture( preview, sourceSize, 2.0, QRectF(windowRect), {{{windowRect}, QStringLiteral("w1"), QStringLiteral("hidpi")}}); - CaptureEditor editor(capture); + CaptureEditor editor(capture, CaptureEditor::CaptureMode::Window); prepareSelectEditor(editor, widget); - QTest::keyClick(&editor, Qt::Key_Space); // Region -> Window QTest::mouseMove(&editor, QPoint(100, 90), 20); QApplication::processEvents(); const QImage ui = editor.grab().toImage(); @@ -449,14 +446,22 @@ bool runMeasurementReadoutCheck(QString &error) { if (!expect(QStringLiteral("300, 240"), QStringLiteral("Idle pointer"))) return false; - QTest::keyClick(&editor, Qt::Key_Space); // Region -> Window - QTest::mouseMove(&editor, QPoint(200, 150), 20); - QApplication::processEvents(); - if (!expect(QStringLiteral("600 × 440"), QStringLiteral("Hovered window"))) - return false; - QTest::keyClick(&editor, Qt::Key_Space); // Window -> Scroll - QTest::keyClick(&editor, Qt::Key_Space); // Scroll -> Region - QApplication::processEvents(); + { + CaptureEditor windowEditor(capture, CaptureEditor::CaptureMode::Window); + windowEditor.setSuppressSnapshots(true); + windowEditor.resize(800, 600); + windowEditor.show(); + QApplication::processEvents(); + QTest::mouseMove(&windowEditor, QPoint(200, 150), 20); + QApplication::processEvents(); + if (windowEditor.measurementText() != QStringLiteral("600 × 440")) { + error = QStringLiteral("Hovered window readout was \"%1\", expected " + "\"600 × 440\"") + .arg(windowEditor.measurementText()); + return false; + } + windowEditor.close(); + } QTest::mousePress(&editor, Qt::LeftButton, Qt::NoModifier, QPoint(150, 120)); QApplication::processEvents(); @@ -483,6 +488,119 @@ bool runMeasurementReadoutCheck(QString &error) { return true; } +/** Smart selection infers a click target but keeps a real drag freeform. */ +bool runSmartSelectionSmoke(QApplication &application, QString &error) { + CaptureData capture; + capture.monitor.name = QStringLiteral("TEST"); + capture.monitor.geometry = QRect(0, 0, 800, 600); + capture.monitor.pixelSize = QSize(800, 600); + capture.monitor.scale = 1.0; + capture.source = QImage(800, 600, QImage::Format_ARGB32_Premultiplied); + capture.source.fill(QColor(QStringLiteral("#406080"))); + capture.previewSize = capture.source.size(); + const QRect windowRect(80, 80, 300, 220); + capture.windows = {{windowRect, QStringLiteral("w1"), QStringLiteral("first"), + QStringLiteral("test-app")}}; + + const auto prepare = [&application](CaptureEditor &editor) { + editor.setSuppressSnapshots(true); + editor.resize(800, 600); + editor.show(); + application.processEvents(); + }; + + CaptureEditor windowEditor(capture, CaptureEditor::CaptureMode::Smart); + prepare(windowEditor); + if (!windowEditor.smartModeForTest()) { + error = QStringLiteral("Smart capture did not enter inference mode"); + return false; + } + if (!windowEditor.statusForTest().isEmpty()) { + error = QStringLiteral("Smart capture still showed an instruction pill"); + return false; + } + QTest::mouseMove(&windowEditor, QPoint(200, 160), 20); + application.processEvents(); + const QImage windowUi = windowEditor.grab().toImage(); + const QColor sourceColor(QStringLiteral("#406080")); + if (!colorNear(grabLogicalPixel(windowUi, windowEditor, QPointF(300, 250)), + sourceColor, 2) || + !colorNear(grabLogicalPixel(windowUi, windowEditor, QPointF(700, 350)), + dimmedSelectColor(sourceColor), 2)) { + error = QStringLiteral( + "Smart window target was not the only lit part of the monitor"); + return false; + } + // A few pixels of pointer jitter still count as the old picker's bare click + // (area below 20 px²), rather than producing an accidental tiny crop. + QTest::mousePress(&windowEditor, Qt::LeftButton, Qt::NoModifier, + QPoint(200, 160)); + QTest::mouseMove(&windowEditor, QPoint(203, 163), 20); + QTest::mouseRelease(&windowEditor, Qt::LeftButton, Qt::NoModifier, + QPoint(203, 163)); + application.processEvents(); + if (!windowEditor.editingForTest() || + windowEditor.currentSelection() != QRectF(windowRect)) { + error = QStringLiteral("Smart click did not select the window underneath"); + return false; + } + windowEditor.close(); + + CaptureEditor fullscreenEditor(capture, CaptureEditor::CaptureMode::Smart); + prepare(fullscreenEditor); + QTest::mouseMove(&fullscreenEditor, QPoint(700, 500), 20); + application.processEvents(); + const QImage fullscreenUi = fullscreenEditor.grab().toImage(); + if (!colorNear( + grabLogicalPixel(fullscreenUi, fullscreenEditor, QPointF(300, 250)), + sourceColor, 2) || + !colorNear( + grabLogicalPixel(fullscreenUi, fullscreenEditor, QPointF(600, 350)), + sourceColor, 2)) { + error = QStringLiteral("Smart fullscreen target did not light the monitor"); + return false; + } + QTest::mouseClick(&fullscreenEditor, Qt::LeftButton, Qt::NoModifier, + QPoint(700, 500)); + application.processEvents(); + if (!fullscreenEditor.editingForTest() || + fullscreenEditor.currentSelection() != + QRectF(QPointF(), capture.previewSize)) { + error = QStringLiteral( + "Smart click outside a window did not select the full monitor"); + return false; + } + fullscreenEditor.close(); + + CaptureEditor regionEditor(capture, CaptureEditor::CaptureMode::Smart); + prepare(regionEditor); + QTest::mousePress(®ionEditor, Qt::LeftButton, Qt::NoModifier, + QPoint(100, 100)); + QTest::mouseMove(®ionEditor, QPoint(500, 400), 20); + QTest::mouseRelease(®ionEditor, Qt::LeftButton, Qt::NoModifier, + QPoint(500, 400)); + application.processEvents(); + if (!regionEditor.editingForTest() || + regionEditor.currentSelection() != QRectF(100, 100, 400, 300)) { + error = QStringLiteral("Smart drag snapped instead of staying freeform"); + return false; + } + regionEditor.close(); + + CaptureEditor explicitRegion(capture, CaptureEditor::CaptureMode::Region); + prepare(explicitRegion); + QTest::mouseClick(&explicitRegion, Qt::LeftButton, Qt::NoModifier, + QPoint(200, 160)); + application.processEvents(); + if (!explicitRegion.selectingForTest() || + !explicitRegion.currentSelection().isEmpty()) { + error = QStringLiteral("Explicit region mode treated a click as smart"); + return false; + } + explicitRegion.close(); + return true; +} + /** Checks that positional local image targets are recognized. */ bool runPositionalImageTargetCheck(QString &error) { QTemporaryDir directory; @@ -831,8 +949,8 @@ bool runTextAwareHighlighterEditorCheck(QApplication &application, CaptureEditor editor(capture, CaptureEditor::CaptureMode::Fullscreen); editor.setSuppressSnapshots(true); // baseImageRect is exactly 400x240 at (30,135), making test gestures map - // 1:1 to annotation coordinates while leaving the toolbar clear of the - // capture tabs. The source itself remains 2x HiDPI. + // 1:1 to annotation coordinates while leaving the toolbar clear. The + // source itself remains 2x HiDPI. editor.resize(460, 500); editor.show(); application.processEvents(); @@ -1300,6 +1418,11 @@ bool runPointerDamageRegionCheck(QString &error) { capture.previewSize = capture.monitor.pixelSize; capture.source = QImage(32, 18, QImage::Format_ARGB32_Premultiplied); capture.source.fill(QColor(QStringLiteral("#182030"))); + capture.windows = { + {QRect(1000, 700, 800, 600), QStringLiteral("w1"), + QStringLiteral("one"), QStringLiteral("test-app")}, + {QRect(3000, 1600, 900, 700), QStringLiteral("w2"), + QStringLiteral("two"), QStringLiteral("test-app")}}; CaptureEditor editor(capture); editor.resize(capture.previewSize); @@ -1319,6 +1442,22 @@ bool runPointerDamageRegionCheck(QString &error) { .arg(screenPixels); return false; } + + CaptureEditor smart(capture, CaptureEditor::CaptureMode::Smart); + smart.resize(capture.previewSize); + const QRegion betweenWindows = smart.windowHoverDamageForTest(0, 1); + if (!betweenWindows.contains(QPoint(998, 1000)) || + !betweenWindows.contains(QPoint(2998, 1900)) || + betweenWindows == QRegion(smart.rect())) { + error = QStringLiteral( + "Smart window damage did not cover both antialiased outlines locally"); + return false; + } + if (smart.windowHoverDamageForTest(0, -1) != QRegion(smart.rect())) { + error = QStringLiteral( + "Smart window/fullscreen transition did not repaint the monitor"); + return false; + } return true; } @@ -1943,8 +2082,8 @@ bool runTextClickAwayCommitCheck(QApplication &application, QString &error) { QTest::mouseRelease(&editor, Qt::LeftButton, Qt::NoModifier, QPoint(650, 470)); application.processEvents(); // Screen points computed from the real image geometry, not hand-picked - // literals: robust to the toolbar/tab strip's own layout, which this click - // has to land through regardless of how tall it currently is. + // literals: robust to the toolbar's own layout, which this click has to + // land through regardless of how tall it currently is. const QPointF imageOrigin = editor.editImageRectForTest().topLeft(); const qreal editScale = editor.editScaleForTest(); const auto toScreen = [&](const QPointF &annotationPoint) { @@ -2262,7 +2401,7 @@ bool runAnnotationLayerChecks(QApplication &application, QString &error) { // secret sits at annotation (200,200)-(300,260), centered on (250,230). A // loupe smaller than the redaction can only show redacted pixels if it // samples the redaction layer. Screen points are computed from the live - // image geometry (toScreen), not hand-picked, since the toolbar/tab chrome + // image geometry (toScreen), not hand-picked, since the toolbar chrome // above the image changes that geometry. const auto spotlightPreviewColor = [&](bool redact) { CaptureEditor editor(editorCapture, CaptureEditor::CaptureMode::Fullscreen); @@ -2945,8 +3084,8 @@ bool runAsyncCaptureRegionSmoke(QApplication &application, QString &error) { return false; } - // y=60 clears the capture-kind tabs, which on a 320 px wide test surface - // reach almost edge to edge. + // Start below the top edge so this remains a representative region drag on + // the compact surface. QTest::mousePress(&editor, Qt::LeftButton, Qt::NoModifier, QPoint(40, 60)); QTest::mouseMove(&editor, QPoint(200, 180), 20); QTest::mouseRelease(&editor, Qt::LeftButton, Qt::NoModifier, @@ -3122,7 +3261,7 @@ bool runOpLogSmoke(QApplication &application, QString &error) { editor.show(); application.processEvents(); // Screen points computed from the live image geometry, not hand-picked: - // Fullscreen scales the 800x600 capture to fit under the toolbar/tab + // Fullscreen scales the 800x600 capture to fit under the toolbar // chrome, and that scale depends on their current height. const QPointF origin = editor.editImageRectForTest().topLeft(); const qreal scale = editor.editScaleForTest(); @@ -3745,11 +3884,15 @@ bool runStuckModifierSmoke(QApplication &application, QString &error) { // A shallow drag, with the stale Shift the compositor still reports. It must // draw where it was dragged: snapped to 45°, this arrow would come out flat. + const QPoint arrowStart = + editor.toScreenPointForTest(QPointF(100, 295)).toPoint(); + const QPoint arrowEnd = + editor.toScreenPointForTest(QPointF(300, 265)).toPoint(); const QImage before = flushedSnapshot(editor, snapshotPath); - QTest::mousePress(&editor, Qt::LeftButton, Qt::ShiftModifier, QPoint(200, 411)); - QTest::mouseMove(&editor, QPoint(400, 381), 20); + QTest::mousePress(&editor, Qt::LeftButton, Qt::ShiftModifier, arrowStart); + QTest::mouseMove(&editor, arrowEnd, 20); QTest::mouseRelease(&editor, Qt::LeftButton, Qt::ShiftModifier, - QPoint(400, 381)); + arrowEnd); application.processEvents(); const QImage drawn = flushedSnapshot(editor, snapshotPath); if (drawn == before) { @@ -3777,10 +3920,10 @@ bool runStuckModifierSmoke(QApplication &application, QString &error) { // Shift means Shift: the same drag now snaps flat. QTest::keyClick(&editor, Qt::Key_Z, Qt::ControlModifier); application.processEvents(); - QTest::mousePress(&editor, Qt::LeftButton, Qt::ShiftModifier, QPoint(200, 411)); - QTest::mouseMove(&editor, QPoint(400, 381), 20); + QTest::mousePress(&editor, Qt::LeftButton, Qt::ShiftModifier, arrowStart); + QTest::mouseMove(&editor, arrowEnd, 20); QTest::mouseRelease(&editor, Qt::LeftButton, Qt::ShiftModifier, - QPoint(400, 381)); + arrowEnd); application.processEvents(); const QImage snapped = flushedSnapshot(editor, snapshotPath); const auto snappedInk = [&snapped](int x, int y) { @@ -7084,10 +7227,10 @@ bool runViewportZoomSmoke(QApplication &application, QString &error) { return true; } -/** The capture-kind tabs across the top: clicking Window and Region moves - * between the two modes Space toggles, and Fullscreen selects the monitor the - * way Ctrl+A does. */ -bool runSelectTabsSmoke(QApplication &application, QString &error) { +/** Smart capture has no mode switcher: the former top strip is ordinary + * selection space and Space does not change capture behavior. Scroll capture + * remains reachable through S/--scroll and the editor's explicit button. */ +bool runCaptureControlsSmoke(QApplication &application, QString &error) { CaptureData capture; capture.monitor.name = QStringLiteral("TEST"); capture.monitor.geometry = {0, 0, 800, 600}; @@ -7099,59 +7242,33 @@ bool runSelectTabsSmoke(QApplication &application, QString &error) { capture.windows = {{QRect(100, 100, 300, 200), QStringLiteral("w1"), QStringLiteral("One"), QStringLiteral("firefox")}}; - CaptureEditor editor(capture); + CaptureEditor editor(capture, CaptureEditor::CaptureMode::Smart); editor.resize(800, 600); editor.show(); application.processEvents(); - const auto clickOn = [&](CaptureEditor &target, const QString &label) { - const QRectF tab = target.selectTabRectForTest(label); - if (tab.isNull()) - return false; - const QPoint at = tab.center().toPoint(); - QTest::mouseMove(&target, at); - QTest::mouseClick(&target, Qt::LeftButton, Qt::NoModifier, at); - application.processEvents(); - return true; - }; - const auto click = [&](const QString &label) { return clickOn(editor, label); }; - if (editor.windowModeForTest()) { - error = QStringLiteral("Select overlay did not start in region mode"); - return false; - } - if (!click(QStringLiteral("WINDOW")) || !editor.windowModeForTest()) { - error = QStringLiteral("Window tab did not enter window mode"); - return false; - } - if (!click(QStringLiteral("REGION")) || editor.windowModeForTest()) { - error = QStringLiteral("Region tab did not return to region mode"); - return false; - } - if (!click(QStringLiteral("FULLSCREEN")) || editor.selectingForTest() || - editor.renderCurrentOutput().size() != QSize(800, 600)) { - error = QStringLiteral("Fullscreen tab did not select the whole monitor"); - return false; - } - // The strip stays in the edit phase as the way back; a tab there drops the - // edit and returns to the select phase in that mode. - if (!click(QStringLiteral("WINDOW")) || !editor.selectingForTest() || - !editor.windowModeForTest()) { - error = QStringLiteral("Window tab from the editor did not return to " - "window selection"); + QTest::keyClick(&editor, Qt::Key_Space); + application.processEvents(); + if (!editor.smartModeForTest() || !editor.selectingForTest() || + editor.windowModeForTest() || editor.scrollModeForTest()) { + error = QStringLiteral("Space still changed the smart capture mode"); return false; } - if (!click(QStringLiteral("FULLSCREEN")) || editor.selectingForTest()) - return false; - if (!click(QStringLiteral("REGION")) || !editor.selectingForTest() || - editor.windowModeForTest() || editor.annotationCountForTest() != 0) { - error = QStringLiteral("Region tab from the editor did not return to a " - "clean region selection"); + const QPoint formerSwitcher(400, 18); + QTest::mousePress(&editor, Qt::LeftButton, Qt::NoModifier, formerSwitcher); + QTest::mouseMove(&editor, QPoint(520, 100), 20); + QTest::mouseRelease(&editor, Qt::LeftButton, Qt::NoModifier, + QPoint(520, 100)); + application.processEvents(); + if (!editor.editingForTest() || + editor.currentSelection() != QRectF(400, 18, 120, 82)) { + error = QStringLiteral( + "The removed top switcher still intercepted region selection"); return false; } editor.close(); - // Scrolling Region is a mode of the same surface: drawing a region in it - // brings the scroll panel up in place; its tabs leave it; dismissing it - // returns to selecting in scroll mode. + // Drawing in explicit scroll mode brings the panel up in place; dismissing + // it returns to selecting in that same mode. { CaptureEditor scrollEditor(capture, CaptureEditor::CaptureMode::Scroll); scrollEditor.resize(800, 600); @@ -7161,6 +7278,12 @@ bool runSelectTabsSmoke(QApplication &application, QString &error) { error = QStringLiteral("--scroll did not open selecting in scroll mode"); return false; } + QTest::keyClick(&scrollEditor, Qt::Key_Space); + application.processEvents(); + if (!scrollEditor.scrollModeForTest()) { + error = QStringLiteral("Space changed explicit scroll mode"); + return false; + } QTest::mousePress(&scrollEditor, Qt::LeftButton, Qt::NoModifier, QPoint(100, 100)); QTest::mouseMove(&scrollEditor, QPoint(500, 400), 20); @@ -7173,22 +7296,6 @@ bool runSelectTabsSmoke(QApplication &application, QString &error) { "panel up"); return false; } - // Region and Scrolling Region frame the same rectangle, so switching - // between them keeps it: the frame drawn for the scroll panel is the - // region that gets captured. - if (!clickOn(scrollEditor, QStringLiteral("REGION")) || - !scrollEditor.editingForTest() || - scrollEditor.renderCurrentOutput().size() != QSize(400, 300)) { - error = QStringLiteral("Region tab did not carry the scroll frame over"); - return false; - } - // And back again: the region just captured frames the scroll panel. - if (!clickOn(scrollEditor, QStringLiteral("SCROLLING REGION")) || - !scrollEditor.scrollPanelActiveForTest()) { - error = QStringLiteral("Scrolling Region tab did not carry the region " - "over"); - return false; - } QTest::keyClick(QApplication::focusWidget(), Qt::Key_Escape); application.processEvents(); if (scrollEditor.scrollPanelActiveForTest() || @@ -7197,23 +7304,6 @@ bool runSelectTabsSmoke(QApplication &application, QString &error) { "selecting a scrolling region"); return false; } - // Space walks Region -> Window -> Scroll -> Region. Starting in scroll, - // the next step is Region (Fullscreen is skipped). - QTest::keyClick(&scrollEditor, Qt::Key_Space); - if (scrollEditor.windowModeForTest() || scrollEditor.scrollModeForTest()) { - error = QStringLiteral("Space from scroll mode did not step to region"); - return false; - } - QTest::keyClick(&scrollEditor, Qt::Key_Space); - if (!scrollEditor.windowModeForTest() || scrollEditor.scrollModeForTest()) { - error = QStringLiteral("Space from region did not step to window"); - return false; - } - QTest::keyClick(&scrollEditor, Qt::Key_Space); - if (!scrollEditor.scrollModeForTest()) { - error = QStringLiteral("Space did not cycle back round to scroll mode"); - return false; - } // A stitched result is handed to the same editor and annotates like any // capture: the whole image is the selection, a drawn layer renders on it, // and Esc then steps back rather than closing (the editor, not selecting). @@ -7244,28 +7334,24 @@ bool runSelectTabsSmoke(QApplication &application, QString &error) { error = QStringLiteral("Esc in the editor closed or left it"); return false; } - // Back to selecting through the tab: a handed image is not the screen, - // so the monitor is captured again and selection resumes in scroll mode. - if (!clickOn(scrollEditor, QStringLiteral("SCROLLING REGION")) || - !scrollEditor.selectingForTest() || !scrollEditor.scrollModeForTest()) { - error = QStringLiteral("Tab from a stitched edit did not return to " - "selecting a scrolling region"); + const QRectF scrollPill = scrollEditor.scrollPillRectForTest(); + if (scrollPill.isNull()) { + error = QStringLiteral("Stitched edit did not offer scroll capture"); + return false; + } + QTest::mouseClick(&scrollEditor, Qt::LeftButton, Qt::NoModifier, + scrollPill.center().toPoint()); + application.processEvents(); + if (!scrollEditor.selectingForTest() || + !scrollEditor.scrollModeForTest() || + !scrollEditor.scrollPanelActiveForTest()) { + error = QStringLiteral( + "Scroll capture button did not reopen the region in the panel"); return false; } scrollEditor.close(); application.processEvents(); } - - // A file has no screen to go back to: no tabs in its editor. - CaptureEditor fileEditor(capture, CaptureEditor::CaptureMode::File); - fileEditor.resize(800, 600); - fileEditor.show(); - application.processEvents(); - if (!fileEditor.selectTabRectForTest(QStringLiteral("REGION")).isNull()) { - error = QStringLiteral("Tabs are offered when editing a file"); - return false; - } - fileEditor.close(); return true; } @@ -7579,6 +7665,10 @@ int main(int argc, char **argv) { qWarning().noquote() << snapshotError; return 95; } + if (!runSmartSelectionSmoke(application, snapshotError)) { + qWarning().noquote() << snapshotError; + return 136; + } if (!runPointerDamageRegionCheck(snapshotError)) { qWarning().noquote() << snapshotError; return 135; @@ -7688,7 +7778,7 @@ int main(int argc, char **argv) { qWarning().noquote() << snapshotError; return 118; } - if (!runSelectTabsSmoke(application, snapshotError)) { + if (!runCaptureControlsSmoke(application, snapshotError)) { qWarning().noquote() << snapshotError; return 127; } @@ -8053,6 +8143,8 @@ int main(int argc, char **argv) { return 75; } QDir(savedRoot).removeRecursively(); + QImage hoverUi; + QImage keyboardWindowUi; { CaptureData cropCapture; @@ -8081,27 +8173,33 @@ int main(int argc, char **argv) { QFile::remove(snapshotPath); } + { + CaptureEditor windowNavigationEditor( + capture, CaptureEditor::CaptureMode::Window); + windowNavigationEditor.resize(800, 600); + windowNavigationEditor.show(); + application.processEvents(); + QTest::mouseMove(&windowNavigationEditor, QPoint(200, 160), 20); + application.processEvents(); + hoverUi = windowNavigationEditor.grab().toImage(); + if (hoverUi.pixelColor(200, 160) != + capture.source.pixelColor(200, 160)) + return 7; + QTest::keyClick(&windowNavigationEditor, Qt::Key_Right, Qt::MetaModifier); + application.processEvents(); + keyboardWindowUi = windowNavigationEditor.grab().toImage(); + if (keyboardWindowUi.pixelColor(500, 200) != + capture.source.pixelColor(500, 200) || + keyboardWindowUi.pixelColor(200, 160) == + capture.source.pixelColor(200, 160)) + return 8; + windowNavigationEditor.close(); + } - CaptureEditor editor(capture); + CaptureEditor editor(capture, CaptureEditor::CaptureMode::Region); editor.resize(800, 600); editor.show(); application.processEvents(); - QTest::keyClick(&editor, Qt::Key_Space); // Region -> Window - QTest::mouseMove(&editor, QPoint(200, 160), 20); - application.processEvents(); - const QImage hoverUi = editor.grab().toImage(); - if (hoverUi.pixelColor(200, 160) != capture.source.pixelColor(200, 160)) - return 7; - QTest::keyClick(&editor, Qt::Key_Right, Qt::MetaModifier); - application.processEvents(); - const QImage keyboardWindowUi = editor.grab().toImage(); - if (keyboardWindowUi.pixelColor(500, 200) != - capture.source.pixelColor(500, 200) || - keyboardWindowUi.pixelColor(200, 160) == - capture.source.pixelColor(200, 160)) - return 8; - QTest::keyClick(&editor, Qt::Key_Space); // Window -> Scroll - QTest::keyClick(&editor, Qt::Key_Space); // Scroll -> Region QTest::mousePress(&editor, Qt::LeftButton, Qt::NoModifier, QPoint(100, 100)); QTest::mouseMove(&editor, QPoint(650, 470), 20); QTest::mouseRelease(&editor, Qt::LeftButton, Qt::NoModifier,