From 0d8c0b0fbaa9ba61c19933631ba10f67aaf01fe7 Mon Sep 17 00:00:00 2001 From: svan71 <48870638+svan71@users.noreply.github.com> Date: Fri, 17 Jul 2026 02:52:01 -0400 Subject: [PATCH] fix: keep Treeland screenshot toolbar on screen --- src/CMakeLists.txt | 2 + src/main_window.cpp | 64 +++++- src/src.pro | 2 + src/utils/treelandtoolbarplacement.cpp | 126 ++++++++++++ src/utils/treelandtoolbarplacement.h | 59 ++++++ .../test_all_interfaces.h | 1 + .../ut_screen_shot_recorder.pro | 3 + .../utils/ut_treelandtoolbarplacement.h | 186 ++++++++++++++++++ 8 files changed, 433 insertions(+), 10 deletions(-) create mode 100644 src/utils/treelandtoolbarplacement.cpp create mode 100644 src/utils/treelandtoolbarplacement.h create mode 100644 tests/ut_screen_shot_recorder/utils/ut_treelandtoolbarplacement.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f84d2b5da..7bbed50b1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -81,6 +81,7 @@ set ( deepin-screen-recorder_HDRS widgets/tooltips.h widgets/filter.h utils/screengrabber.h + utils/treelandtoolbarplacement.h dbusinterface/drawinterface.h screen_shot_event.h lib/GifH/gif.h @@ -145,6 +146,7 @@ set ( deepin-screen-recorder_SRCS widgets/filter.cpp utils/desktopinfo.cpp utils/screengrabber.cpp + utils/treelandtoolbarplacement.cpp dbusinterface/drawinterface.cpp screen_shot_event.cpp RecorderRegionShow.cpp diff --git a/src/main_window.cpp b/src/main_window.cpp index 72b19a770..67dd50340 100755 --- a/src/main_window.cpp +++ b/src/main_window.cpp @@ -21,6 +21,7 @@ #include "utils/configsettings.h" #include "utils/shortcut.h" #include "utils/screengrabber.h" +#include "utils/treelandtoolbarplacement.h" #include "utils/log.h" #include "camera_process.h" #include "widgets/tooltips.h" @@ -8081,7 +8082,24 @@ void MainWindow::updateCaptureRegion() recordHeight = region.height(); m_toolBar->showWidget(); - const QPoint pos(region.x(), qMin(region.bottom(), height() - 2 * m_toolBar->height())); + m_toolBar->adjustSize(); + + QList screens; + const auto qScreens = QGuiApplication::screens(); + screens.reserve(qScreens.size()); + for (const QScreen *screen : qScreens) { + if (screen) { + screens.append(screen->geometry()); + } + } + QRect fallback = geometry(); + if (fallback.isEmpty() && QGuiApplication::primaryScreen()) { + fallback = QGuiApplication::primaryScreen()->geometry(); + } + const QRect screenGeom = + TreelandToolBarPlacement::pickScreenGeometry(region, screens, fallback); + const QPoint pos = TreelandToolBarPlacement::placeToolBar( + region, m_toolBar->size(), screenGeom, TOOLBAR_Y_SPACING); m_toolBar->showAt(pos); if (windowHandle() && m_toolBar->windowHandle() @@ -8289,21 +8307,48 @@ void MainWindow::onTreelandSwitchToShotUI() m_toolBar->setFocusPolicy(Qt::NoFocus); m_toolBar->setWindowFlags(m_toolBar->windowFlags() | Qt::Tool | Qt::WindowStaysOnTopHint | Qt::FramelessWindowHint); + QList screens; + const auto qScreens = QGuiApplication::screens(); + screens.reserve(qScreens.size()); + for (const QScreen *screen : qScreens) { + if (screen) { + screens.append(screen->geometry()); + } + } + QRect fallback = geometry(); #if (QT_VERSION_MAJOR == 5) - const QRect scr = QApplication::desktop()->screenGeometry(); + if (fallback.isEmpty()) { + fallback = QApplication::desktop()->screenGeometry(); + } #elif (QT_VERSION_MAJOR == 6) - const QRect scr = QGuiApplication::primaryScreen() ? QGuiApplication::primaryScreen()->geometry() : this->geometry(); + if (fallback.isEmpty() && QGuiApplication::primaryScreen()) { + fallback = QGuiApplication::primaryScreen()->geometry(); + } #endif + const QRect restoreRegion = m_hasLastTreelandShotRegion + ? m_lastTreelandShotRegion + : QRect(recordX, recordY, recordWidth, recordHeight); + const QRect screenGeom = + TreelandToolBarPlacement::pickScreenGeometry(restoreRegion, screens, fallback); + + m_toolBar->hide(); + m_toolBar->showWidget(); + m_toolBar->adjustSize(); + QPoint targetPos; if (m_hasLastTreelandShotToolBarPos) { - targetPos = m_lastTreelandShotToolBarPos; + targetPos = TreelandToolBarPlacement::clampToScreen( + m_lastTreelandShotToolBarPos, m_toolBar->size(), screenGeom); + } else if (!restoreRegion.isEmpty() && restoreRegion.isValid()) { + targetPos = TreelandToolBarPlacement::placeToolBar( + restoreRegion, m_toolBar->size(), screenGeom, TOOLBAR_Y_SPACING); } else { - targetPos = QPoint(scr.x() + scr.width() / 2 - m_toolBar->width() / 2, - scr.y() + scr.height() / 2 - m_toolBar->height() / 2); + targetPos = TreelandToolBarPlacement::clampToScreen( + QPoint(screenGeom.x() + screenGeom.width() / 2 - m_toolBar->width() / 2, + screenGeom.y() + screenGeom.height() / 2 - m_toolBar->height() / 2), + m_toolBar->size(), + screenGeom); } - m_toolBar->hide(); - m_toolBar->showWidget(); - m_toolBar->adjustSize(); m_toolBar->showAt(targetPos); m_toolBar->raise(); m_toolBar->show(); @@ -8354,4 +8399,3 @@ void MainWindow::initAudioAndCameraWatchers() qCDebug(dsrApp) << "摄像头监视器初始化完成"; } } - diff --git a/src/src.pro b/src/src.pro index 97f55a9d6..7c0c7a2ce 100755 --- a/src/src.pro +++ b/src/src.pro @@ -216,6 +216,7 @@ HEADERS += main_window.h \ widgets/tooltips.h \ widgets/filter.h \ utils/screengrabber.h \ + utils/treelandtoolbarplacement.h \ RecorderRegionShow.h \ recordertablet.h \ dbusinterface/ocrinterface.h \ @@ -312,6 +313,7 @@ SOURCES += main.cpp \ widgets/tooltips.cpp \ widgets/filter.cpp \ utils/screengrabber.cpp \ + utils/treelandtoolbarplacement.cpp \ RecorderRegionShow.cpp \ recordertablet.cpp \ dbusinterface/ocrinterface.cpp \ diff --git a/src/utils/treelandtoolbarplacement.cpp b/src/utils/treelandtoolbarplacement.cpp new file mode 100644 index 000000000..eb20c6720 --- /dev/null +++ b/src/utils/treelandtoolbarplacement.cpp @@ -0,0 +1,126 @@ +// SPDX-FileCopyrightText: 2026 svan71 +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "treelandtoolbarplacement.h" + +#include +#include + +namespace TreelandToolBarPlacement { + +namespace { + +bool toolbarFitsAt(const QPoint &topLeft, + const QSize &toolBarSize, + const QRect &screenGeom) +{ + if (toolBarSize.width() <= 0 || toolBarSize.height() <= 0 || screenGeom.isEmpty()) { + return false; + } + const QRect bar(topLeft, toolBarSize); + return screenGeom.contains(bar); +} + +} // namespace + +QRect pickScreenGeometry(const QRect &captureRegion, + const QList &screens, + const QRect &fallback) +{ + if (screens.isEmpty()) { + return fallback; + } + + const QPoint center = captureRegion.center(); + for (const QRect &screen : screens) { + if (!screen.isEmpty() && screen.contains(center)) { + return screen; + } + } + + QRect best; + int bestArea = -1; + for (const QRect &screen : screens) { + if (screen.isEmpty()) { + continue; + } + const QRect inter = screen.intersected(captureRegion); + const int area = inter.isEmpty() ? 0 : inter.width() * inter.height(); + if (area > bestArea) { + bestArea = area; + best = screen; + } + } + + if (bestArea > 0 && !best.isEmpty()) { + return best; + } + + // No intersection: pick the screen whose center is nearest the capture center. + best = screens.first(); + qint64 bestDist = std::numeric_limits::max(); + for (const QRect &screen : screens) { + if (screen.isEmpty()) { + continue; + } + const QPoint sc = screen.center(); + const qint64 dx = qint64(sc.x()) - center.x(); + const qint64 dy = qint64(sc.y()) - center.y(); + const qint64 dist = dx * dx + dy * dy; + if (dist < bestDist) { + bestDist = dist; + best = screen; + } + } + return best.isEmpty() ? fallback : best; +} + +QPoint clampToScreen(const QPoint &topLeft, + const QSize &toolBarSize, + const QRect &screenGeom) +{ + if (screenGeom.isEmpty() || toolBarSize.width() <= 0 || toolBarSize.height() <= 0) { + return topLeft; + } + + const int maxX = screenGeom.x() + std::max(0, screenGeom.width() - toolBarSize.width()); + const int maxY = screenGeom.y() + std::max(0, screenGeom.height() - toolBarSize.height()); + const int x = std::clamp(topLeft.x(), screenGeom.x(), maxX); + const int y = std::clamp(topLeft.y(), screenGeom.y(), maxY); + return QPoint(x, y); +} + +QPoint placeToolBar(const QRect &captureRegion, + const QSize &toolBarSize, + const QRect &screenGeom, + int ySpacing) +{ + if (screenGeom.isEmpty() || toolBarSize.width() <= 0 || toolBarSize.height() <= 0) { + return clampToScreen(captureRegion.topLeft(), toolBarSize, screenGeom); + } + + // Right-align to the capture region (same preference as updateToolBarPos on X11). + const int preferredX = captureRegion.x() + captureRegion.width() - toolBarSize.width(); + const int x = clampToScreen(QPoint(preferredX, screenGeom.y()), toolBarSize, screenGeom).x(); + + const int spacing = std::max(0, ySpacing); + + // Prefer outside below the selection. + const int yBelow = captureRegion.y() + captureRegion.height() + spacing; + if (toolbarFitsAt(QPoint(x, yBelow), toolBarSize, screenGeom)) { + return QPoint(x, yBelow); + } + + // Else outside above the selection. + const int yAbove = captureRegion.y() - toolBarSize.height() - spacing; + if (toolbarFitsAt(QPoint(x, yAbove), toolBarSize, screenGeom)) { + return QPoint(x, yAbove); + } + + // Neither outside slot fits: place inside the capture near its top, then clamp. + const int yInside = captureRegion.y() + spacing; + return clampToScreen(QPoint(x, yInside), toolBarSize, screenGeom); +} + +} // namespace TreelandToolBarPlacement diff --git a/src/utils/treelandtoolbarplacement.h b/src/utils/treelandtoolbarplacement.h new file mode 100644 index 000000000..20d7f1e31 --- /dev/null +++ b/src/utils/treelandtoolbarplacement.h @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2026 svan71 +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef TREELANDTOOLBARPLACEMENT_H +#define TREELANDTOOLBARPLACEMENT_H + +#include +#include +#include +#include + +/** + * Pure geometry helpers for Treeland screenshot toolbar placement. + * + * Coordinates are logical pixels in the same space as the Treeland capture + * region and the MainWindow overlay (typically virtual-desktop origin). + * Kept free of compositor / widget state so unit tests can exercise edge cases. + */ +namespace TreelandToolBarPlacement { + +/** Default gap between capture rect and outside toolbar placement. */ +inline constexpr int kDefaultYSpacing = 5; + +/** + * Pick the screen that should own the toolbar for @p captureRegion. + * Prefers the screen containing the region center; falls back to the screen + * with the largest intersection area; finally @p fallback. + */ +QRect pickScreenGeometry(const QRect &captureRegion, + const QList &screens, + const QRect &fallback); + +/** + * Clamp a top-left toolbar position so the full toolbar rectangle stays inside + * @p screenGeom. If the toolbar is larger than the screen on an axis, pin that + * axis to the screen origin (best-effort visibility). + */ +QPoint clampToScreen(const QPoint &topLeft, + const QSize &toolBarSize, + const QRect &screenGeom); + +/** + * Deterministic Treeland screenshot toolbar placement: + * - horizontal: right-align to the capture rect (legacy X11 behavior), then clamp; + * - vertical: prefer outside below with @p ySpacing when the full toolbar fits + * on the chosen screen; else outside above when it fits; else inside the + * capture (top + spacing) and clamp. + * Always returns a position where the full toolbar rect is inside @p screenGeom + * whenever toolBarSize fits on that screen. + */ +QPoint placeToolBar(const QRect &captureRegion, + const QSize &toolBarSize, + const QRect &screenGeom, + int ySpacing = kDefaultYSpacing); + +} // namespace TreelandToolBarPlacement + +#endif // TREELANDTOOLBARPLACEMENT_H diff --git a/tests/ut_screen_shot_recorder/test_all_interfaces.h b/tests/ut_screen_shot_recorder/test_all_interfaces.h index 7ef2648d4..61587bc50 100644 --- a/tests/ut_screen_shot_recorder/test_all_interfaces.h +++ b/tests/ut_screen_shot_recorder/test_all_interfaces.h @@ -21,6 +21,7 @@ #include "utils/ut_configsettings.h" //#include "utils/ut_desktopinfo.h" #include "utils/ut_screengrabber.h" +#include "utils/ut_treelandtoolbarplacement.h" #include "utils/ut_shortcut.h" #include "utils/ut_tempfile.h" #include "utils/ut_utils_other.h" diff --git a/tests/ut_screen_shot_recorder/ut_screen_shot_recorder.pro b/tests/ut_screen_shot_recorder/ut_screen_shot_recorder.pro index 838c60016..370ba194f 100644 --- a/tests/ut_screen_shot_recorder/ut_screen_shot_recorder.pro +++ b/tests/ut_screen_shot_recorder/ut_screen_shot_recorder.pro @@ -115,6 +115,7 @@ HEADERS += test_all_interfaces.h \ #utils/ut_dbusutils.h \ #utils/ut_desktopinfo.h \ utils/ut_screengrabber.h \ + utils/ut_treelandtoolbarplacement.h \ utils/ut_shortcut.h \ utils/ut_tempfile.h \ utils/ut_utils_other.h \ @@ -129,6 +130,7 @@ HEADERS += test_all_interfaces.h \ #../../src/utils/dbusutils.h \ #../../src/utils/desktopinfo.h \ ../../src/utils/screengrabber.h \ + ../../src/utils/treelandtoolbarplacement.h \ ../../src/utils/shortcut.h \ ../../src/utils/tempfile.h \ ../../src/utils/shapesutils.h \ @@ -336,6 +338,7 @@ SOURCES += main.cpp \ ../../src/utils/borderprocessinterface.cpp \ #../../src/utils/desktopinfo.cpp \ ../../src/utils/screengrabber.cpp \ + ../../src/utils/treelandtoolbarplacement.cpp \ ../../src/utils/shortcut.cpp \ ../../src/utils/tempfile.cpp \ ../../src/utils/shapesutils.cpp \ diff --git a/tests/ut_screen_shot_recorder/utils/ut_treelandtoolbarplacement.h b/tests/ut_screen_shot_recorder/utils/ut_treelandtoolbarplacement.h new file mode 100644 index 000000000..f71c59cc8 --- /dev/null +++ b/tests/ut_screen_shot_recorder/utils/ut_treelandtoolbarplacement.h @@ -0,0 +1,186 @@ +// SPDX-FileCopyrightText: 2026 svan71 +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include +#include +#include +#include + +#include "../../../src/utils/treelandtoolbarplacement.h" + +using namespace testing; +using TreelandToolBarPlacement::clampToScreen; +using TreelandToolBarPlacement::pickScreenGeometry; +using TreelandToolBarPlacement::placeToolBar; + +namespace { + +const QSize kBar(400, 68); +const int kGap = 5; + +bool fullyOnScreen(const QPoint &pos, const QSize &size, const QRect &screen) +{ + return screen.contains(QRect(pos, size)); +} + +bool outsideCapture(const QPoint &pos, const QSize &size, const QRect &capture) +{ + return !QRect(pos, size).intersects(capture); +} + +} // namespace + +class TreelandToolBarPlacementTest : public testing::Test +{ +}; + +// Top-right selection on a 1920x1080 primary: toolbar must sit fully on-screen, +// prefer below when that fits, and not hang past the right/top edges. +TEST_F(TreelandToolBarPlacementTest, topRightSelectionClampsAndPrefersBelow) +{ + const QRect screen(0, 0, 1920, 1080); + const QRect capture(1700, 40, 180, 120); // near top-right + const QPoint pos = placeToolBar(capture, kBar, screen, kGap); + + EXPECT_TRUE(fullyOnScreen(pos, kBar, screen)); + // Right-aligned to capture: 1700+180-400=1480; fully on-screen (not past 1520). + EXPECT_EQ(pos.x(), capture.x() + capture.width() - kBar.width()); + EXPECT_LE(pos.x() + kBar.width(), screen.x() + screen.width()); + // Below fits (40+120+5+68 < 1080) + EXPECT_EQ(pos.y(), capture.y() + capture.height() + kGap); + EXPECT_TRUE(outsideCapture(pos, kBar, capture)); +} + +// Narrow strip against the right edge: X clamped; full width still on screen. +TEST_F(TreelandToolBarPlacementTest, rightEdgeNarrowSelection) +{ + const QRect screen(0, 0, 1920, 1080); + const QRect capture(1880, 400, 40, 80); + const QPoint pos = placeToolBar(capture, kBar, screen, kGap); + + EXPECT_TRUE(fullyOnScreen(pos, kBar, screen)); + EXPECT_EQ(pos.x(), 1920 - kBar.width()); + EXPECT_EQ(pos.y(), capture.y() + capture.height() + kGap); + EXPECT_TRUE(outsideCapture(pos, kBar, capture)); +} + +// Bottom-right: below does not fit → place above when it fits. +TEST_F(TreelandToolBarPlacementTest, bottomRightPrefersAbove) +{ + const QRect screen(0, 0, 1920, 1080); + const QRect capture(1600, 980, 280, 90); + const QPoint pos = placeToolBar(capture, kBar, screen, kGap); + + EXPECT_TRUE(fullyOnScreen(pos, kBar, screen)); + EXPECT_EQ(pos.y(), capture.y() - kBar.height() - kGap); + EXPECT_TRUE(outsideCapture(pos, kBar, capture)); + EXPECT_LE(pos.x() + kBar.width(), screen.x() + screen.width()); +} + +// Flush against the top edge with no room above: below preferred when available. +TEST_F(TreelandToolBarPlacementTest, topEdgePlacesBelow) +{ + const QRect screen(0, 0, 1920, 1080); + const QRect capture(200, 0, 300, 100); + const QPoint pos = placeToolBar(capture, kBar, screen, kGap); + + EXPECT_TRUE(fullyOnScreen(pos, kBar, screen)); + EXPECT_EQ(pos.y(), capture.y() + capture.height() + kGap); + EXPECT_TRUE(outsideCapture(pos, kBar, capture)); + // Right-aligned to capture, not overflowing left. + EXPECT_EQ(pos.x(), capture.x() + capture.width() - kBar.width()); +} + +// Selection wider than toolbar: right-align keeps bar inside capture's right edge. +TEST_F(TreelandToolBarPlacementTest, selectionWiderThanToolbarRightAligns) +{ + const QRect screen(0, 0, 1920, 1080); + const QRect capture(100, 200, 900, 300); + const QPoint pos = placeToolBar(capture, kBar, screen, kGap); + + EXPECT_TRUE(fullyOnScreen(pos, kBar, screen)); + EXPECT_EQ(pos.x(), capture.x() + capture.width() - kBar.width()); + EXPECT_EQ(pos.y(), capture.y() + capture.height() + kGap); + EXPECT_TRUE(outsideCapture(pos, kBar, capture)); +} + +// Selection narrower than toolbar: X may extend left of capture, still on screen. +TEST_F(TreelandToolBarPlacementTest, selectionNarrowerThanToolbarStillOnScreen) +{ + const QRect screen(0, 0, 1920, 1080); + const QRect capture(500, 300, 80, 60); + const QPoint pos = placeToolBar(capture, kBar, screen, kGap); + + EXPECT_TRUE(fullyOnScreen(pos, kBar, screen)); + EXPECT_EQ(pos.x(), capture.x() + capture.width() - kBar.width()); + EXPECT_LT(pos.x(), capture.x()); + EXPECT_TRUE(outsideCapture(pos, kBar, capture)); +} + +// Multi-screen with non-zero origin: pick screen containing capture center. +TEST_F(TreelandToolBarPlacementTest, multiScreenNonZeroOrigin) +{ + const QRect left(0, 0, 1920, 1080); + const QRect right(1920, 0, 2560, 1440); + const QList screens{left, right}; + const QRect capture(1920 + 2300, 20, 200, 100); // top-right of second screen + + const QRect picked = pickScreenGeometry(capture, screens, left); + EXPECT_EQ(picked, right); + + const QPoint pos = placeToolBar(capture, kBar, picked, kGap); + EXPECT_TRUE(fullyOnScreen(pos, kBar, right)); + EXPECT_FALSE(left.intersects(QRect(pos, kBar))); // must not spill onto left screen + EXPECT_EQ(pos.x(), capture.x() + capture.width() - kBar.width()); + EXPECT_GE(pos.x(), right.x()); + EXPECT_LE(pos.x() + kBar.width(), right.x() + right.width()); + EXPECT_EQ(pos.y(), capture.y() + capture.height() + kGap); + EXPECT_TRUE(outsideCapture(pos, kBar, capture)); +} + +// Neither above nor below fits (capture nearly full screen height): place inside and clamp. +TEST_F(TreelandToolBarPlacementTest, fullHeightCapturePlacesInsideClamped) +{ + const QRect screen(0, 0, 1920, 1080); + const QRect capture(100, 10, 800, 1060); + const QPoint pos = placeToolBar(capture, kBar, screen, kGap); + + EXPECT_TRUE(fullyOnScreen(pos, kBar, screen)); + // Inside candidate is capture.y()+gap; must remain on screen. + EXPECT_GE(pos.y(), screen.y()); + EXPECT_LE(pos.y() + kBar.height(), screen.y() + screen.height()); +} + +// clampToScreen must pull a stale edge position back fully on-screen. +TEST_F(TreelandToolBarPlacementTest, clampRestoredEdgePosition) +{ + const QRect screen(0, 0, 1920, 1080); + const QPoint stale(1900, -20); // past right and top + const QPoint clamped = clampToScreen(stale, kBar, screen); + + EXPECT_TRUE(fullyOnScreen(clamped, kBar, screen)); + EXPECT_EQ(clamped.x(), 1920 - kBar.width()); + EXPECT_EQ(clamped.y(), 0); +} + +// Screen taller secondary monitor origin below primary (vertical stack). +TEST_F(TreelandToolBarPlacementTest, verticalStackScreenOrigin) +{ + const QRect top(0, 0, 1920, 1080); + const QRect bottom(0, 1080, 1920, 1200); + const QList screens{top, bottom}; + const QRect capture(1500, 1080 + 50, 300, 100); + + const QRect picked = pickScreenGeometry(capture, screens, top); + EXPECT_EQ(picked, bottom); + + const QPoint pos = placeToolBar(capture, kBar, picked, kGap); + EXPECT_TRUE(fullyOnScreen(pos, kBar, bottom)); + EXPECT_EQ(pos.y(), capture.y() + capture.height() + kGap); + EXPECT_TRUE(outsideCapture(pos, kBar, capture)); +}