diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c95d17e9..8a6994f8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -37,6 +37,8 @@ target_sources(spectacle PRIVATE Gui/ExportMenu.cpp Gui/HelpMenu.cpp Gui/OptionsMenu.cpp + Gui/RecordingModeMenu.cpp + Gui/ScreenshotModeMenu.cpp Gui/SmartSpinBox.cpp Gui/Selection.cpp Gui/SelectionEditor.cpp @@ -147,6 +149,7 @@ set_source_files_properties(Gui/QmlUtils.qml PROPERTIES qt_target_qml_sources(spectacle QML_FILES + Gui/AcceptAction.qml Gui/AnimatedLoader.qml Gui/AnnotationOptionsToolBarContents.qml Gui/Annotations/AnnotationEditor.qml @@ -160,38 +163,49 @@ qt_target_qml_sources(spectacle Gui/CaptureOptions.qml Gui/CaptureSettingsColumn.qml Gui/CopiedMessage.qml + Gui/CopyImageAction.qml + Gui/CopyLocationAction.qml Gui/DashedOutline.qml Gui/DelaySpinBox.qml Gui/DialogPage.qml + Gui/EditAction.qml Gui/EmptyPage.qml + Gui/ExportMenuButton.qml Gui/FloatingBackground.qml Gui/FloatingToolBar.qml Gui/Handle.qml + Gui/HelpMenuButton.qml Gui/ImageCaptureOverlay.qml Gui/ImageView.qml Gui/InlineMessage.qml Gui/LocationCopiedMessage.qml Gui/Magnifier.qml - Gui/MainToolBarContents.qml + Gui/NewScreenshotToolButton.qml + Gui/OptionsMenuButton.qml Gui/Outline.qml Gui/QRCodeScannedMessage.qml Gui/QmlUtils.qml + Gui/RecordAction.qml Gui/RecordOptions.qml Gui/RecordingFailedMessage.qml Gui/RecordingModeButtonsColumn.qml + Gui/RecordingModeMenuButton.qml Gui/RecordingSettingsColumn.qml Gui/RecordingView.qml + Gui/SaveAction.qml + Gui/SaveAsAction.qml Gui/SavedAndCopiedMessage.qml Gui/SavedAndLocationCopied.qml Gui/SavedMessage.qml Gui/ScreenshotFailedMessage.qml + Gui/ScreenshotModeMenuButton.qml Gui/ScreenshotView.qml Gui/ShareErrorMessage.qml Gui/SharedMessage.qml Gui/ShortcutsTextBox.qml Gui/SizeLabel.qml + Gui/TtToolButton.qml Gui/UndoRedoGroup.qml - Gui/VideoCaptureOverlay.qml ) install(TARGETS spectacle ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/src/CaptureModeModel.cpp b/src/CaptureModeModel.cpp index 1b7720dc..2037b97f 100644 --- a/src/CaptureModeModel.cpp +++ b/src/CaptureModeModel.cpp @@ -15,6 +15,16 @@ using namespace Qt::StringLiterals; +static std::unique_ptr s_instance; + +CaptureModeModel *CaptureModeModel::instance() +{ + if (!s_instance) { + s_instance = std::make_unique(); + } + return s_instance.get(); +} + static QString actionShortcutsToString(QAction *action) { QString value; diff --git a/src/CaptureModeModel.h b/src/CaptureModeModel.h index 574936cb..ef78756a 100644 --- a/src/CaptureModeModel.h +++ b/src/CaptureModeModel.h @@ -7,6 +7,7 @@ #include "Platforms/ImagePlatform.h" #include +#include /** * This is a model containing the current supported capture modes and their labels and shortcuts. @@ -15,11 +16,23 @@ class CaptureModeModel : public QAbstractListModel { Q_OBJECT QML_ELEMENT + QML_SINGLETON Q_PROPERTY(int count READ rowCount NOTIFY countChanged FINAL) public: CaptureModeModel(QObject *parent = nullptr); + static CaptureModeModel *instance(); + + static CaptureModeModel *create(QQmlEngine *engine, QJSEngine *) + { + auto inst = instance(); + Q_ASSERT(inst); + Q_ASSERT(inst->thread() == engine->thread()); + QJSEngine::setObjectOwnership(inst, QJSEngine::CppOwnership); + return inst; + } + enum CaptureMode { RectangularRegion, AllScreens, diff --git a/src/Gui/AcceptAction.qml b/src/Gui/AcceptAction.qml new file mode 100644 index 00000000..436066bc --- /dev/null +++ b/src/Gui/AcceptAction.qml @@ -0,0 +1,11 @@ +/* SPDX-FileCopyrightText: 2025 Noah Davis + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick.Templates as T + +T.Action { + icon.name: "dialog-ok" + text: i18nc("@action accept selection", "Accept") + onTriggered: contextWindow.accept() +} diff --git a/src/Gui/AnnotationOptionsToolBarContents.qml b/src/Gui/AnnotationOptionsToolBarContents.qml index 89198ac1..e7428127 100644 --- a/src/Gui/AnnotationOptionsToolBarContents.qml +++ b/src/Gui/AnnotationOptionsToolBarContents.qml @@ -38,14 +38,9 @@ Row { } } - component ToolButton: QQC.ToolButton { - implicitHeight: QmlUtils.iconTextButtonHeight - width: display === QQC.ToolButton.IconOnly ? height : implicitWidth + component ToolButton: TtToolButton { focusPolicy: root.focusPolicy display: root.displayMode - QQC.ToolTip.text: text - QQC.ToolTip.visible: (hovered || pressed) && display === QQC.ToolButton.IconOnly - QQC.ToolTip.delay: Kirigami.Units.toolTipDelay } Loader { // stroke diff --git a/src/Gui/Annotations/CropTool.qml b/src/Gui/Annotations/CropTool.qml index c17557da..80b3588c 100644 --- a/src/Gui/Annotations/CropTool.qml +++ b/src/Gui/Annotations/CropTool.qml @@ -153,6 +153,7 @@ Loader { } Outline { + pathHints: ShapePath.PathLinear x: selectionRect.x - strokeWidth y: selectionRect.y - strokeWidth width: selectionRect.width + strokeWidth * 2 diff --git a/src/Gui/Annotations/TextTool.qml b/src/Gui/Annotations/TextTool.qml index a2def4a8..b687b7fb 100644 --- a/src/Gui/Annotations/TextTool.qml +++ b/src/Gui/Annotations/TextTool.qml @@ -183,6 +183,7 @@ AnimatedLoader { topInset: -background.strokeWidth bottomInset: -background.strokeWidth background: DashedOutline { + pathHints: ShapePath.PathLinear strokeWidth: QmlUtils.clampPx(dprRound(1) / root.viewport.scale) } diff --git a/src/Gui/ButtonGrid.qml b/src/Gui/ButtonGrid.qml index bb158446..a3e7b834 100644 --- a/src/Gui/ButtonGrid.qml +++ b/src/Gui/ButtonGrid.qml @@ -12,14 +12,23 @@ Grid { property int displayMode: QQC.AbstractButton.TextBesideIcon property int focusPolicy: Qt.StrongFocus readonly property bool mirrored: effectiveLayoutDirection === Qt.RightToLeft - property bool animationsEnabled: true + property bool animationsEnabled: false clip: childrenRect.width > width || childrenRect.height > height horizontalItemAlignment: Grid.AlignHCenter verticalItemAlignment: Grid.AlignVCenter spacing: Kirigami.Units.mediumSpacing - columns: flow === Grid.LeftToRight ? visibleChildren.length : 1 - rows: flow === Grid.TopToBottom ? visibleChildren.length : 1 + /* Using -1 for either rows or columns sets the amount to unlimited, + * but not if you set both to -1. Using `visibleChildren.length` to set + * unlimited rows or columns can generate errors about not having enough + * rows/columns when a child item's `visible` property is toggled. + * Internally, rows and columns are set to defaults like this: + * if (rows <= 0 && columns <= 0) { columns = 4; rows = (numVisible+3)/4; } + * else if (rows <= 0) { rows = (numVisible+(columns-1))/columns; } + * else if (columns <= 0) { columns = (numVisible+(rows-1))/rows; } + */ + columns: flow === Grid.LeftToRight ? -1 : 1 + rows: flow === Grid.TopToBottom ? -1 : 1 move: Transition { enabled: root.animationsEnabled NumberAnimation { properties: "x,y"; duration: Kirigami.Units.longDuration; easing.type: Easing.OutCubic } diff --git a/src/Gui/CaptureModeButtonsColumn.qml b/src/Gui/CaptureModeButtonsColumn.qml index 08b78be1..9071a5e6 100644 --- a/src/Gui/CaptureModeButtonsColumn.qml +++ b/src/Gui/CaptureModeButtonsColumn.qml @@ -11,7 +11,7 @@ import org.kde.spectacle.private ColumnLayout { spacing: Kirigami.Units.mediumSpacing Repeater { - model: CaptureModeModel { } + model: CaptureModeModel delegate: QQC.DelayButton { id: button readonly property bool showCancel: Settings.captureMode === model.captureMode && SpectacleCore.captureTimeRemaining > 0 diff --git a/src/Gui/CaptureWindow.cpp b/src/Gui/CaptureWindow.cpp index 462a1344..7568b8c4 100644 --- a/src/Gui/CaptureWindow.cpp +++ b/src/Gui/CaptureWindow.cpp @@ -107,25 +107,14 @@ QScreen *CaptureWindow::screenToFollow() const void CaptureWindow::setMode(CaptureWindow::Mode mode) { - if (mode == Image) { - syncGeometryWithScreen(); - QVariantMap initialProperties = { - // Set the parent in initialProperties to avoid having - // the parent and window be null in Component.onCompleted - {u"parent"_s, QVariant::fromValue(contentItem())} - }; - setSource(QUrl("%1/Gui/ImageCaptureOverlay.qml"_L1.arg(SPECTACLE_QML_PATH)), - initialProperties); - } else if (mode == Video) { - syncGeometryWithScreen(); - QVariantMap initialProperties = { - // Set the parent in initialProperties to avoid having - // the parent and window be null in Component.onCompleted - {u"parent"_s, QVariant::fromValue(contentItem())} - }; - setSource(QUrl("%1/Gui/VideoCaptureOverlay.qml"_L1.arg(SPECTACLE_QML_PATH)), - initialProperties); - } + syncGeometryWithScreen(); + QVariantMap initialProperties = { + // Set the parent in initialProperties to avoid having + // the parent and window be null in Component.onCompleted + {u"parent"_s, QVariant::fromValue(contentItem())} + }; + setSource(QUrl("%1/Gui/ImageCaptureOverlay.qml"_L1.arg(SPECTACLE_QML_PATH)), + initialProperties); } bool CaptureWindow::accept() diff --git a/src/Gui/CopyImageAction.qml b/src/Gui/CopyImageAction.qml new file mode 100644 index 00000000..8a4718a9 --- /dev/null +++ b/src/Gui/CopyImageAction.qml @@ -0,0 +1,15 @@ +/* SPDX-FileCopyrightText: 2025 Noah Davis + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick.Templates as T +import org.kde.spectacle.private + +T.Action { + // We don't use this in video mode because you can't copy raw video to the + // clipboard, or at least not elegantly. + enabled: !SpectacleCore.videoMode + icon.name: "edit-copy" + text: i18nc("@action", "Copy") + onTriggered: contextWindow.copyImage() +} diff --git a/src/Gui/CopyLocationAction.qml b/src/Gui/CopyLocationAction.qml new file mode 100644 index 00000000..d52255f0 --- /dev/null +++ b/src/Gui/CopyLocationAction.qml @@ -0,0 +1,11 @@ +/* SPDX-FileCopyrightText: 2025 Noah Davis + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick.Templates as T + +T.Action { + icon.name: "edit-copy-path" + text: i18nc("@action", "Copy Location") + onTriggered: contextWindow.copyLocation() +} diff --git a/src/Gui/DashedOutline.qml b/src/Gui/DashedOutline.qml index d3d7d40d..a1067305 100644 --- a/src/Gui/DashedOutline.qml +++ b/src/Gui/DashedOutline.qml @@ -19,6 +19,7 @@ Outline { property alias dashOffset: dashPath.dashOffset property alias dashSvgPath: dashPathSvg.path property alias dashPathScale: dashPath.scale + property alias dashPathHints: dashPath.pathHints // A regular alternative pattern with a spacing in logical pixels function regularDashPattern(spacing, strokeWidth = root.strokeWidth) { @@ -38,6 +39,7 @@ Outline { capStyle: ShapePath.FlatCap joinStyle: root.joinStyle scale: root.pathScale + pathHints: root.pathHints PathSvg { id: dashPathSvg path: root.svgPath diff --git a/src/Gui/EditAction.qml b/src/Gui/EditAction.qml new file mode 100644 index 00000000..988af740 --- /dev/null +++ b/src/Gui/EditAction.qml @@ -0,0 +1,15 @@ +/* SPDX-FileCopyrightText: 2025 Noah Davis + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick.Templates as T +import org.kde.spectacle.private + +T.Action { + enabled: !SpectacleCore.videoMode + icon.name: "edit-image" + text: i18nc("@action edit screenshot", "Edit…") + checkable: true + checked: contextWindow.annotating + onToggled: contextWindow.annotating = checked +} diff --git a/src/Gui/ExportMenu.cpp b/src/Gui/ExportMenu.cpp index d994179c..1a1a041c 100644 --- a/src/Gui/ExportMenu.cpp +++ b/src/Gui/ExportMenu.cpp @@ -47,6 +47,8 @@ ExportMenu::ExportMenu(QWidget *parent) , mPurposeMenu(new Purpose::Menu) #endif { + setTitle(i18nc("@title:menu", "Export")); + setIcon(QIcon::fromTheme(u"document-share"_s)); addAction(QIcon::fromTheme(u"document-open-folder"_s), i18n("Open Default Screenshots Folder"), this, &ExportMenu::openScreenshotsFolder); diff --git a/src/Gui/ExportMenuButton.qml b/src/Gui/ExportMenuButton.qml new file mode 100644 index 00000000..769f8053 --- /dev/null +++ b/src/Gui/ExportMenuButton.qml @@ -0,0 +1,17 @@ +/* SPDX-FileCopyrightText: 2025 Noah Davis + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami +import org.kde.spectacle.private + +TtToolButton { + // FIXME: make export menu actually work with videos + visible: !SpectacleCore.videoMode + icon.name: "document-share" + text: i18nc("@action", "Export") + down: pressed || ExportMenu.visible + Accessible.role: Accessible.ButtonMenu + onPressed: ExportMenu.popup(this) +} diff --git a/src/Gui/FloatingToolBar.qml b/src/Gui/FloatingToolBar.qml index a42fc3dc..3fe9d1d8 100644 --- a/src/Gui/FloatingToolBar.qml +++ b/src/Gui/FloatingToolBar.qml @@ -14,6 +14,7 @@ T.Pane { property real topRightRadius: radius property real bottomLeftRadius: radius property real bottomRightRadius: radius + property real backgroundColorOpacity: 0.95 implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, contentWidth + leftPadding + rightPadding) @@ -26,7 +27,7 @@ T.Pane { background: FloatingBackground { color: Qt.rgba(root.palette.window.r, root.palette.window.g, - root.palette.window.b, 0.85) + root.palette.window.b, root.backgroundColorOpacity) border.color: Qt.rgba(root.palette.windowText.r, root.palette.windowText.g, root.palette.windowText.b, 0.2) diff --git a/src/Gui/Handle.qml b/src/Gui/Handle.qml index fbb386d0..a6c241ba 100644 --- a/src/Gui/Handle.qml +++ b/src/Gui/Handle.qml @@ -153,6 +153,9 @@ Shape { strokeStyle: root.strokeStyle joinStyle: root.joinStyle capStyle: root.capStyle + pathHints: (Math.abs(root.sweepAngle) === 360 || Math.abs(root.sweepAngle) <= 180 + ? ShapePath.PathConvex : ShapePath.PathSolid) + | ShapePath.PathNonOverlappingControlPointTriangles PathAngleArc { moveToStart: true // this path should not be affected by startX/startY radiusX: root.radiusX diff --git a/src/Gui/HelpMenu.cpp b/src/Gui/HelpMenu.cpp index cadeb22c..ccc66adc 100644 --- a/src/Gui/HelpMenu.cpp +++ b/src/Gui/HelpMenu.cpp @@ -6,6 +6,7 @@ #include "WidgetWindowUtils.h" #include +#include #include #include @@ -13,6 +14,8 @@ #include +using namespace Qt::StringLiterals; + class HelpMenuSingleton { public: @@ -39,6 +42,8 @@ HelpMenu::HelpMenu(QWidget* parent) : SpectacleMenu(parent) , kHelpMenu(new KHelpMenu(parent, KAboutData::applicationData(), true)) { + setTitle(i18nc("@title:menu", "Help")); + setIcon(QIcon::fromTheme(u"help-contents"_s)); addActions(kHelpMenu->menu()->actions()); connect(this, &QMenu::triggered, this, &HelpMenu::onTriggered); } diff --git a/src/Gui/HelpMenuButton.qml b/src/Gui/HelpMenuButton.qml new file mode 100644 index 00000000..be30b32f --- /dev/null +++ b/src/Gui/HelpMenuButton.qml @@ -0,0 +1,14 @@ +/* SPDX-FileCopyrightText: 2025 Noah Davis + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.spectacle.private + +TtToolButton { + icon.name: "help-contents" + text: i18nc("@action", "Help") + down: pressed || HelpMenu.visible + Accessible.role: Accessible.ButtonMenu + onPressed: HelpMenu.popup(this) +} diff --git a/src/Gui/ImageCaptureOverlay.qml b/src/Gui/ImageCaptureOverlay.qml index d94a649c..da4cf33d 100644 --- a/src/Gui/ImageCaptureOverlay.qml +++ b/src/Gui/ImageCaptureOverlay.qml @@ -6,6 +6,7 @@ import QtQuick import QtQuick.Window import QtQuick.Layouts +import QtQuick.Shapes import QtQuick.Controls as QQC import org.kde.kirigami as Kirigami import org.kde.spectacle.private @@ -25,42 +26,58 @@ import "Annotations" MouseArea { // This needs to be a mousearea in orcer for the proper mouse events to be correctly filtered id: root + readonly property rect viewportRect: Geometry.mapFromPlatformRect(screenToFollow.geometry, screenToFollow.devicePixelRatio) + readonly property AnnotationDocument document: annotationsLoader.item?.document ?? null focus: true acceptedButtons: Qt.LeftButton | Qt.RightButton hoverEnabled: true LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft LayoutMirroring.childrenInherit: true anchors.fill: parent + enabled: !SpectacleCore.videoPlatform.isRecording - readonly property Selection selection: SelectionEditor.selection - - AnnotationEditor { - id: annotations + AnimatedLoader { + id: annotationsLoader anchors.fill: parent - visible: true - enabled: contextWindow.annotating - viewportRect: Geometry.mapFromPlatformRect(screenToFollow.geometry, screenToFollow.devicePixelRatio) + state: !SpectacleCore.videoMode ? "active" : "inactive" + animationDuration: Kirigami.Units.veryLongDuration + sourceComponent: AnnotationEditor { + enabled: contextWindow.annotating + viewportRect: root.viewportRect + } + } + + component ToolButton : TtToolButton { + focusPolicy: Qt.NoFocus + } + + component ToolBarSizeLabel: SizeLabel { + height: QmlUtils.iconTextButtonHeight + size: { + const sz = SelectionEditor.selection.empty + ? Qt.size(SelectionEditor.screensRect.width, + SelectionEditor.screensRect.height) + : SelectionEditor.selection.size + return Geometry.rawSize(sz, SelectionEditor.devicePixelRatio) + } + leftPadding: Kirigami.Units.mediumSpacing + QmlUtils.fontMetrics.descent + rightPadding: leftPadding } component Overlay: Rectangle { color: Settings.useLightMaskColor ? "white" : "black" - opacity: 0.5 - LayoutMirroring.enabled: false - } - Overlay { // top / full overlay when nothing selected - id: topOverlay - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: selectionRectangle.visible ? selectionRectangle.top : parent.bottom - opacity: if (root.selection.empty - && (!annotations.document.tool.isNoTool || annotations.document.undoStackDepth > 0)) { + opacity: if (SpectacleCore.videoPlatform.isRecording + || (annotationsLoader.visible + && SelectionEditor.selection.empty + && root.document && (!root.document.tool.isNoTool + || root.document.undoStackDepth > 0))) { return 0 - } else if (root.selection.empty) { + } else if (SelectionEditor.selection.empty) { return 0.25 } else { return 0.5 } + LayoutMirroring.enabled: false Behavior on opacity { NumberAnimation { duration: Kirigami.Units.longDuration @@ -68,6 +85,13 @@ MouseArea { } } } + Overlay { // top / full overlay when nothing selected + id: topOverlay + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: selectionRectangle.visible ? selectionRectangle.top : parent.bottom + } Overlay { // bottom id: bottomOverlay anchors.left: parent.left @@ -95,37 +119,69 @@ MouseArea { visible: selectionRectangle.visible && height > 0 && width > 0 } - Rectangle { + AnimatedLoader { + anchors.centerIn: parent + visible: opacity > 0 && !SpectacleCore.videoPlatform.isRecording + state: topOverlay.opacity === 0.25 ? "active" : "inactive" + sourceComponent: Kirigami.Heading { + id: cropToolHelp + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: i18nc("@info basic crop tool explanation", "Click and drag to make a selection.") + padding: cropToolHelpMetrics.height - cropToolHelpMetrics.descent + leftPadding: cropToolHelpMetrics.height + rightPadding: cropToolHelpMetrics.height + background: FloatingBackground { + radius: cropToolHelpMetrics.height + color: Qt.alpha(palette.window, 0.9) + } + FontMetrics { + id: cropToolHelpMetrics + font: cropToolHelp.font + } + } + } + + + DashedOutline { id: selectionRectangle - enabled: annotations.document.tool.isNoTool - color: "transparent" - border.color: if (enabled) { + // We need to be a bit careful about staying out of the recorded area + function roundPos(value: real) : real { + return SpectacleCore.videoPlatform.isRecording ? dprFloor(value - (strokeWidth + 1 / Screen.devicePixelRatio)) : dprRound(value - strokeWidth) + } + function roundSize(value: real) : real { + return SpectacleCore.videoPlatform.isRecording ? dprCeil(value + (strokeWidth + 1 / Screen.devicePixelRatio)) : dprRound(value + strokeWidth) + } + pathHints: ShapePath.PathLinear + dashSvgPath: SpectacleCore.videoPlatform.isRecording ? svgPath : "" + visible: !SelectionEditor.selection.empty + && Geometry.rectIntersects(Qt.rect(x,y,width,height), Qt.rect(0,0,parent.width, parent.height)) + enabled: root.document?.tool.isNoTool || SpectacleCore.videoMode + strokeWidth: dprRound(1) + strokeColor: if (enabled) { return palette.active.highlight } else if (Settings.useLightMaskColor) { return "black" } else { return "white" } - border.width: contextWindow.dprRound(1) - visible: !root.selection.empty && Geometry.rectIntersects(Qt.rect(x,y,width,height), Qt.rect(0,0,parent.width, parent.height)) - x: root.selection.x - border.width - annotations.viewportRect.x - y: root.selection.y - border.width - annotations.viewportRect.y - width: root.selection.width + border.width * 2 - height: root.selection.height + border.width * 2 - - LayoutMirroring.enabled: false - LayoutMirroring.childrenInherit: true + dashColor: SpectacleCore.videoPlatform.isRecording ? palette.active.base : strokeColor + // We need to be a bit careful about staying out of the recorded area + x: roundPos(SelectionEditor.selection.x - root.viewportRect.x) + y: roundPos(SelectionEditor.selection.y - root.viewportRect.y) + width: roundSize(SelectionEditor.selection.right - root.viewportRect.x) - x + height: roundSize(SelectionEditor.selection.bottom - root.viewportRect.y) - y } Item { - x: -annotations.viewportRect.x - y: -annotations.viewportRect.y + x: -root.viewportRect.x + y: -root.viewportRect.y enabled: selectionRectangle.enabled component SelectionHandle: Handle { visible: enabled && selectionRectangle.visible && SelectionEditor.dragLocation === SelectionEditor.None - && Geometry.rectIntersects(Qt.rect(x,y,width,height), annotations.viewportRect) - fillColor: selectionRectangle.border.color + && Geometry.rectIntersects(Qt.rect(x,y,width,height), root.viewportRect) + fillColor: selectionRectangle.strokeColor } SelectionHandle { @@ -170,34 +226,23 @@ MouseArea { } } - ShortcutsTextBox { - anchors { - horizontalCenter: parent.horizontalCenter - bottom: parent.bottom - } - visible: opacity > 0 && Settings.showCaptureInstructions - // Assume SelectionEditor covers all screens. - // Use parent's coordinate system. - opacity: root.containsMouse - && !contains(mapFromItem(root, root.mouseX, root.mouseY)) - && !root.pressed - && annotations.document.tool.isNoTool - && !mtbDragHandler.active - && !atbDragHandler.active - && !Geometry.rectIntersects(SelectionEditor.handlesRect, Qt.rect(x, y, width, height)) - Behavior on opacity { - NumberAnimation { - duration: Kirigami.Units.longDuration - easing.type: Easing.OutCubic - } - } - } - Item { // separate item because it needs to be above the stuff defined above + id: screensRectItem + readonly property bool allowToolbars: { + let emptyHovered = (root.containsMouse || annotationsLoader.item?.hovered) && SelectionEditor.selection.empty + let menuVisible = ExportMenu.visible + menuVisible |= OptionsMenu.visible + menuVisible |= HelpMenu.visible + menuVisible |= ScreenshotModeMenu.visible + menuVisible |= RecordingModeMenu.visible + let pressed = SelectionEditor.dragLocation || annotationsLoader.item?.anyPressed + return !SpectacleCore.videoPlatform.isRecording && !pressed + && (emptyHovered || !SelectionEditor.selection.empty || menuVisible) + } width: SelectionEditor.screensRect.width height: SelectionEditor.screensRect.height - x: -annotations.viewportRect.x - y: -annotations.viewportRect.y + x: -root.viewportRect.x + y: -root.viewportRect.y // Magnifier Loader { @@ -211,33 +256,33 @@ MouseArea { if (SelectionEditor.magnifierLocation === SelectionEditor.TopLeft || SelectionEditor.magnifierLocation === SelectionEditor.Left || SelectionEditor.magnifierLocation === SelectionEditor.BottomLeft) { - x = root.selection.left + x = SelectionEditor.selection.left } else if (SelectionEditor.magnifierLocation === SelectionEditor.TopRight || SelectionEditor.magnifierLocation === SelectionEditor.Right || SelectionEditor.magnifierLocation === SelectionEditor.BottomRight) { - x = root.selection.right + x = SelectionEditor.selection.right } else if (SelectionEditor.magnifierLocation === SelectionEditor.Top || SelectionEditor.magnifierLocation === SelectionEditor.Bottom) { if (SelectionEditor.dragLocation !== SelectionEditor.None) { x = SelectionEditor.mousePosition.x } else { - x = root.selection.horizontalCenter + x = SelectionEditor.selection.horizontalCenter } } if (SelectionEditor.magnifierLocation === SelectionEditor.TopLeft || SelectionEditor.magnifierLocation === SelectionEditor.Top || SelectionEditor.magnifierLocation === SelectionEditor.TopRight) { - y = root.selection.top + y = SelectionEditor.selection.top } else if (SelectionEditor.magnifierLocation === SelectionEditor.BottomLeft || SelectionEditor.magnifierLocation === SelectionEditor.Bottom || SelectionEditor.magnifierLocation === SelectionEditor.BottomRight) { - y = root.selection.bottom + y = SelectionEditor.selection.bottom } else if (SelectionEditor.magnifierLocation === SelectionEditor.Left || SelectionEditor.magnifierLocation === SelectionEditor.Right) { if (SelectionEditor.dragLocation !== SelectionEditor.None) { y = SelectionEditor.mousePosition.y } else { - y = root.selection.verticalCenter + y = SelectionEditor.selection.verticalCenter } } return Qt.point(x, y) @@ -281,91 +326,30 @@ MouseArea { z: 100 visible: SelectionEditor.showMagnifier && SelectionEditor.magnifierLocation !== SelectionEditor.None - && Geometry.rectIntersects(rect, annotations.viewportRect) - active: Settings.showMagnifier !== Settings.ShowMagnifierNever + && Geometry.rectIntersects(rect, root.viewportRect) + active: Settings.showMagnifier !== Settings.ShowMagnifierNever && annotationsLoader.item !== null sourceComponent: Magnifier { - viewport: annotations + viewport: annotationsLoader.item targetPoint: magnifierLoader.targetPoint } } - // Size ToolTip - SizeLabel { - id: ssToolTip - readonly property int valignment: { - if (root.selection.empty) { - return Qt.AlignVCenter - } - const margin = Kirigami.Units.mediumSpacing * 2 - const w = width + margin - const h = height + margin - if (SelectionEditor.handlesRect.top >= h) { - return Qt.AlignTop - } else if (SelectionEditor.screensRect.height - SelectionEditor.handlesRect.bottom >= h) { - return Qt.AlignBottom - } else { - // At the bottom of the inside of the selection rect. - return Qt.AlignBaseline - } - } - readonly property bool normallyVisible: !root.selection.empty && !(mainToolBar.visible && mainToolBar.valignment === ssToolTip.valignment) - Binding on x { - value: contextWindow.dprRound(root.selection.horizontalCenter - ssToolTip.width / 2) - when: ssToolTip.normallyVisible - restoreMode: Binding.RestoreNone - } - Binding on y { - value: { - let v = 0 - if (ssToolTip.valignment & Qt.AlignBaseline) { - v = Math.min(root.selection.bottom, SelectionEditor.handlesRect.bottom - Kirigami.Units.gridUnit) - - ssToolTip.height - Kirigami.Units.mediumSpacing * 2 - } else if (ssToolTip.valignment & Qt.AlignTop) { - v = SelectionEditor.handlesRect.top - - ssToolTip.height - Kirigami.Units.mediumSpacing * 2 - } else if (ssToolTip.valignment & Qt.AlignBottom) { - v = SelectionEditor.handlesRect.bottom + Kirigami.Units.mediumSpacing * 2 - } else { - v = (root.height - ssToolTip.height) / 2 - parent.y - } - return contextWindow.dprRound(v) - } - when: ssToolTip.normallyVisible - restoreMode: Binding.RestoreNone - } - visible: opacity > 0 - opacity: ssToolTip.normallyVisible - && Geometry.rectIntersects(Qt.rect(x,y,width,height), annotations.viewportRect) - Behavior on opacity { - NumberAnimation { - duration: Kirigami.Units.longDuration - easing.type: Easing.OutCubic - } - } - size: Geometry.rawSize(root.selection.size, SelectionEditor.devicePixelRatio) - padding: Kirigami.Units.mediumSpacing * 2 - topPadding: padding - QmlUtils.fontMetrics.descent - bottomPadding: topPadding - background: FloatingBackground { - implicitWidth: Math.ceil(parent.contentWidth) + parent.leftPadding + parent.rightPadding - implicitHeight: Math.ceil(parent.contentHeight) + parent.topPadding + parent.bottomPadding - color: Qt.rgba(parent.palette.window.r, - parent.palette.window.g, - parent.palette.window.b, 0.85) - border.color: Qt.rgba(parent.palette.windowText.r, - parent.palette.windowText.g, - parent.palette.windowText.b, 0.2) - border.width: contextWindow.dprRound(1) - } - } - Connections { - target: root.selection + target: SelectionEditor.selection function onEmptyChanged() { - if (!root.selection.empty - && (mainToolBar.rememberPosition || atbLoader.rememberPosition)) { - mainToolBar.rememberPosition = false - atbLoader.rememberPosition = false + if (!SelectionEditor.selection.empty) { + if (mainToolBar.rememberPosition) { + mainToolBar.z = 0 + mainToolBar.rememberPosition = false + } + if (ftbLoader.item?.rememberPosition) { + ftbLoader.z = 0 + ftbLoader.item.rememberPosition = false + } + if (atbLoader.item?.rememberPosition) { + atbLoader.z = 0 + atbLoader.item.rememberPosition = false + } } } } @@ -373,65 +357,27 @@ MouseArea { // Main ToolBar FloatingToolBar { id: mainToolBar + readonly property rect rect: Qt.rect(x, y, width, height) property bool rememberPosition: false - readonly property int valignment: { - if (root.selection.empty) { - return 0 - } - if (3 * height + topPadding + Kirigami.Units.mediumSpacing - <= SelectionEditor.screensRect.height - SelectionEditor.handlesRect.bottom - ) { - return Qt.AlignBottom - } else if (3 * height + bottomPadding + Kirigami.Units.mediumSpacing - <= SelectionEditor.handlesRect.top - ) { - return Qt.AlignTop - } else { - // At the bottom of the inside of the selection rect. - return Qt.AlignBaseline - } - } - readonly property bool normallyVisible: { - let emptyHovered = (root.containsMouse || annotations.hovered) && root.selection.empty - let menuVisible = ExportMenu.visible - menuVisible |= OptionsMenu.visible - menuVisible |= HelpMenu.visible - let pressed = SelectionEditor.dragLocation || annotations.anyPressed - return (emptyHovered || !root.selection.empty || menuVisible) && !pressed - } + property alias dragging: mtbDragHandler.active Binding on x { - value: { - const v = root.selection.empty ? (root.width - mainToolBar.width) / 2 + annotations.viewportRect.x - : root.selection.horizontalCenter - mainToolBar.width / 2 - return Math.max(mainToolBar.leftPadding, // min value - Math.min(contextWindow.dprRound(v), - SelectionEditor.screensRect.width - mainToolBar.width - mainToolBar.rightPadding)) // max value - } - when: mainToolBar.normallyVisible && !mainToolBar.rememberPosition + value: Math.max(root.viewportRect.x + mainToolBar.leftPadding, // min value + Math.min(dprRound(root.viewportRect.x + (root.width - mainToolBar.width) / 2), + root.viewportRect.x + root.width - mainToolBar.width - mainToolBar.rightPadding)) // max value + when: screensRectItem.allowToolbars && !mainToolBar.rememberPosition restoreMode: Binding.RestoreNone } Binding on y { - value: { - let v = 0 - // put above selection if not enough room below selection - if (mainToolBar.valignment & Qt.AlignTop) { - v = SelectionEditor.handlesRect.top - - mainToolBar.height - mainToolBar.bottomPadding - } else if (mainToolBar.valignment & Qt.AlignBottom) { - v = SelectionEditor.handlesRect.bottom + mainToolBar.topPadding - } else if (mainToolBar.valignment & Qt.AlignBaseline) { - v = Math.min(root.selection.bottom, SelectionEditor.handlesRect.bottom - Kirigami.Units.gridUnit) - - mainToolBar.height - mainToolBar.bottomPadding - } else { - v = (mainToolBar.height / 2) - mainToolBar.parent.y - } - return contextWindow.dprRound(v) - } - when: mainToolBar.normallyVisible && !mainToolBar.rememberPosition + value: dprRound(root.viewportRect.y + root.height - mainToolBar.height - mainToolBar.bottomPadding) + when: screensRectItem.allowToolbars && !mainToolBar.rememberPosition restoreMode: Binding.RestoreNone } - visible: opacity > 0 - opacity: normallyVisible && Geometry.rectIntersects(Qt.rect(x,y,width,height), annotations.viewportRect) + visible: opacity > 0 && !SpectacleCore.videoPlatform.isRecording + opacity: screensRectItem.allowToolbars + && (mainToolBar.rememberPosition + || SelectionEditor.selection.empty + || (!Geometry.rectIntersects(mainToolBar.rect, ftbLoader.rect) + && !Geometry.rectIntersects(mainToolBar.rect, SelectionEditor.handlesRect))) Behavior on opacity { NumberAnimation { duration: Kirigami.Units.longDuration @@ -440,120 +386,261 @@ MouseArea { } layer.enabled: true // improves the visuals of the opacity animation focusPolicy: Qt.NoFocus - contentItem: MainToolBarContents { - id: mainToolBarContents - focusPolicy: Qt.NoFocus - displayMode: QQC.AbstractButton.TextBesideIcon - showSizeLabel: mainToolBar.valignment === ssToolTip.valignment - imageSize: Geometry.rawSize(root.selection.size, SelectionEditor.devicePixelRatio) + contentItem: ButtonGrid { + spacing: parent.spacing + Loader { + id: mtbImageVideoContentLoader + visible: SelectionEditor.selection.empty + active: visible + sourceComponent: SpectacleCore.videoMode ? videoToolBarComponent : imageMainToolBarComponent + } + QQC.ToolSeparator { + visible: mtbImageVideoContentLoader.visible + height: QmlUtils.iconTextButtonHeight + } + ScreenshotModeMenuButton { + focusPolicy: Qt.NoFocus + } + RecordingModeMenuButton { + focusPolicy: Qt.NoFocus + } + OptionsMenuButton { + focusPolicy: Qt.NoFocus + } } + HoverHandler { + target: mainToolBar + margin: mainToolBar.padding + cursorShape: enabled ? + (mtbDragHandler.active ? Qt.ClosedHandCursor : Qt.OpenHandCursor) + : undefined + } DragHandler { // parent is contentItem and parent is a read-only property id: mtbDragHandler - enabled: root.selection.empty target: mainToolBar acceptedButtons: Qt.LeftButton margin: mainToolBar.padding - xAxis.minimum: annotations.viewportRect.x - xAxis.maximum: annotations.viewportRect.x + root.width - mainToolBar.width - yAxis.minimum: annotations.viewportRect.y - yAxis.maximum: annotations.viewportRect.y + root.height - mainToolBar.height - cursorShape: enabled ? - (active ? Qt.ClosedHandCursor : Qt.OpenHandCursor) - : undefined - onActiveChanged: if (active && !mainToolBar.rememberPosition) { - mainToolBar.rememberPosition = true + xAxis.minimum: root.viewportRect.x + xAxis.maximum: root.viewportRect.x + root.width - mainToolBar.width + yAxis.minimum: root.viewportRect.y + yAxis.maximum: root.viewportRect.y + root.height - mainToolBar.height + onActiveChanged: if (active) { + mainToolBar.z = 2 + atbLoader.z = atbLoader.z > ftbLoader.z ? 1 : 0 + ftbLoader.z = atbLoader.z < ftbLoader.z ? 1 : 0 + if (!mainToolBar.rememberPosition) { + mainToolBar.rememberPosition = true + } } } } - AnimatedLoader { - id: atbLoader - property bool rememberPosition: false - readonly property int valignment: mainToolBar.valignment & (Qt.AlignTop | Qt.AlignBaseline) ? - Qt.AlignTop : Qt.AlignBottom - active: visible && mainToolBar.visible - onActiveChanged: if (!active && rememberPosition - && !contextWindow.annotating) { - rememberPosition = false - } - state: mainToolBar.normallyVisible - && contextWindow.annotating ? "active" : "inactive" - - Binding on x { - value: { - const min = mainToolBar.x - const target = contextWindow.dprRound(mainToolBar.x + (mainToolBar.width - atbLoader.width) / 2) - const max = mainToolBar.x + mainToolBar.width - atbLoader.width - return Math.max(min, Math.min(target, max)) + Component { + id: imageMainToolBarComponent + ButtonGrid { + spacing: parent.parent.spacing + ToolBarSizeLabel {} + ToolButton { + display: TtToolButton.TextBesideIcon + visible: action.enabled + action: AcceptAction {} + } + ToolButton { + display: TtToolButton.IconOnly + visible: action.enabled + action: SaveAction {} + } + ToolButton { + display: TtToolButton.IconOnly + visible: action.enabled + action: SaveAsAction {} + } + ToolButton { + display: TtToolButton.IconOnly + visible: action.enabled + action: CopyImageAction {} + } + ExportMenuButton { + focusPolicy: Qt.NoFocus } - when: !atbLoader.rememberPosition - restoreMode: Binding.RestoreNone } - Binding on y { - value: contextWindow.dprRound(atbLoader.valignment & Qt.AlignTop ? - mainToolBar.y - atbLoader.height - Kirigami.Units.mediumSpacing - : mainToolBar.y + mainToolBar.height + Kirigami.Units.mediumSpacing) - when: !atbLoader.rememberPosition - restoreMode: Binding.RestoreNone + } + Component { + id: imageFinalizerToolBarComponent + ButtonGrid { + spacing: parent.parent.spacing + ToolBarSizeLabel {} + ToolButton { + visible: action.enabled + action: AcceptAction {} + } + ToolButton { + visible: action.enabled + action: SaveAction {} + } + ToolButton { + visible: action.enabled + action: SaveAsAction {} + } + ToolButton { + visible: action.enabled + action: CopyImageAction {} + } + ExportMenuButton { + focusPolicy: Qt.NoFocus + } } - - DragHandler { // parented to contentItem - id: atbDragHandler - enabled: root.selection.empty - acceptedButtons: Qt.LeftButton - xAxis.minimum: annotations.viewportRect.x - xAxis.maximum: annotations.viewportRect.x + root.width - atbLoader.width - yAxis.minimum: annotations.viewportRect.y - yAxis.maximum: annotations.viewportRect.y + root.height - atbLoader.height - cursorShape: enabled ? - (active ? Qt.ClosedHandCursor : Qt.OpenHandCursor) - : undefined - onActiveChanged: if (active && !atbLoader.rememberPosition) { - atbLoader.rememberPosition = true + } + Component { + id: videoToolBarComponent + ButtonGrid { + spacing: parent.parent.spacing + ToolBarSizeLabel {} + ToolButton { + display: TtToolButton.TextBesideIcon + visible: action.enabled + action: RecordAction {} } } + } + Loader { + id: atbLoader + readonly property rect rect: Qt.rect(x, y, width, height) + visible: annotationsLoader.visible + active: visible + z: 2 sourceComponent: FloatingToolBar { id: annotationsToolBar + property bool rememberPosition: false + readonly property int valignment: { + if (SelectionEditor.handlesRect.top >= height + annotationsToolBar.bottomPadding || SelectionEditor.selection.empty) { + // the top of the top side of the selection + // or the top of the screen + return Qt.AlignTop + } else { + // the bottom of the top side of the selection + return Qt.AlignBottom + } + } + property alias dragging: dragHandler.active + visible: opacity > 0 + opacity: screensRectItem.allowToolbars && Geometry.rectIntersects(atbLoader.rect, root.viewportRect) + Behavior on opacity { + NumberAnimation { + duration: Kirigami.Units.longDuration + easing.type: Easing.OutCubic + } + } + // Can't use layer.enabled to improve opacity animation because + // that makes the options toolbar invisible focusPolicy: Qt.NoFocus contentItem: AnnotationsToolBarContents { id: annotationsContents + spacing: parent.spacing displayMode: QQC.AbstractButton.IconOnly focusPolicy: Qt.NoFocus } - topLeftRadius: optionsToolBar.visible - && optionsToolBar.x === 0 - && atbLoader.valignment & Qt.AlignTop ? 0 : radius - topRightRadius: optionsToolBar.visible - && optionsToolBar.x === width - optionsToolBar.width - && atbLoader.valignment & Qt.AlignTop ? 0 : radius - bottomLeftRadius: optionsToolBar.visible - && optionsToolBar.x === 0 - && atbLoader.valignment & Qt.AlignBottom ? 0 : radius - bottomRightRadius: optionsToolBar.visible - && optionsToolBar.x === width - optionsToolBar.width - && atbLoader.valignment & Qt.AlignBottom ? 0 : radius + topLeftRadius: otbLoader.visible + && otbLoader.x === 0 + && (annotationsToolBar.valignment & Qt.AlignTop) + && !SelectionEditor.selection.empty ? 0 : radius + topRightRadius: otbLoader.visible + && otbLoader.x === width - otbLoader.width + && (annotationsToolBar.valignment & Qt.AlignTop) + && !SelectionEditor.selection.empty? 0 : radius + bottomLeftRadius: otbLoader.visible + && otbLoader.x === 0 + && ((annotationsToolBar.valignment & Qt.AlignBottom) + || SelectionEditor.selection.empty) ? 0 : radius + bottomRightRadius: otbLoader.visible + && otbLoader.x === width - otbLoader.width + && ((annotationsToolBar.valignment & Qt.AlignBottom) + || SelectionEditor.selection.empty) ? 0 : radius + + Binding { + property: "x" + target: atbLoader + value: { + const v = SelectionEditor.selection.empty ? (root.width - annotationsToolBar.width) / 2 + root.viewportRect.x + : SelectionEditor.selection.horizontalCenter - annotationsToolBar.width / 2 + return Math.max(annotationsToolBar.leftPadding, // min value + Math.min(dprRound(v), + SelectionEditor.screensRect.width - annotationsToolBar.width - annotationsToolBar.rightPadding)) // max value + } + when: screensRectItem.allowToolbars && !annotationsToolBar.rememberPosition + restoreMode: Binding.RestoreNone + } + Binding { + property: "y" + target: atbLoader + value: { + let v = 0 + if (SelectionEditor.selection.empty) { + v = root.viewportRect.y + annotationsToolBar.topPadding + } else if (annotationsToolBar.valignment & Qt.AlignTop) { + v = SelectionEditor.handlesRect.top - annotationsToolBar.height - annotationsToolBar.bottomPadding + } else if (annotationsToolBar.valignment & Qt.AlignBottom) { + v = Math.max(SelectionEditor.selection.top, SelectionEditor.handlesRect.top + Kirigami.Units.gridUnit) + + annotationsToolBar.topPadding + } else { + v = (root.height - annotationsToolBar.height) / 2 - parent.y + } + return dprRound(v) + } + when: screensRectItem.allowToolbars && !annotationsToolBar.rememberPosition + restoreMode: Binding.RestoreNone + } // Exists purely for cosmetic reasons to make the border of // optionsToolBar that meets annotationsToolBar look better Rectangle { id: borderBg z: -1 - visible: optionsToolBar.visible - opacity: optionsToolBar.opacity + visible: otbLoader.visible + opacity: otbLoader.opacity parent: annotationsToolBar - x: optionsToolBar.x + annotationsToolBar.background.border.width - y: atbLoader.valignment & Qt.AlignTop ? - optionsToolBar.y + optionsToolBar.height : optionsToolBar.y - width: optionsToolBar.width - annotationsToolBar.background.border.width * 2 - height: contextWindow.dprRound(1) + x: otbLoader.x + annotationsToolBar.background.border.width + y: (annotationsToolBar.valignment & Qt.AlignTop) + && !SelectionEditor.selection.empty + ? otbLoader.y + otbLoader.height : otbLoader.y + width: otbLoader.width - annotationsToolBar.background.border.width * 2 + height: dprRound(1) color: annotationsToolBar.background.color } + HoverHandler { + enabled: dragHandler.enabled + target: annotationsToolBar + margin: annotationsToolBar.padding + cursorShape: enabled ? + (dragHandler.active ? Qt.ClosedHandCursor : Qt.OpenHandCursor) + : undefined + } + DragHandler { // parented to contentItem + id: dragHandler + enabled: SelectionEditor.selection.empty || !selectionRectangle.enabled + target: atbLoader + acceptedButtons: Qt.LeftButton + margin: annotationsToolBar.padding + xAxis.minimum: root.viewportRect.x + xAxis.maximum: root.viewportRect.x + root.width - annotationsToolBar.width + yAxis.minimum: root.viewportRect.y + yAxis.maximum: root.viewportRect.y + root.height - annotationsToolBar.height + onActiveChanged: if (active) { + mainToolBar.z = mainToolBar.z > ftbLoader.z ? 1 : 0 + atbLoader.z = 2 + ftbLoader.z = mainToolBar.z < ftbLoader.z ? 1 : 0 + if (!annotationsToolBar.rememberPosition) { + annotationsToolBar.rememberPosition = true + } + } + } + AnimatedLoader { - id: optionsToolBar + id: otbLoader parent: annotationsToolBar x: { let targetX = annotationsContents.x @@ -562,15 +649,16 @@ MouseArea { targetX += checkedButton.x + (checkedButton.width - width) / 2 } return Math.max(0, // min value - Math.min(contextWindow.dprRound(targetX), + Math.min(contextWindow.dprRound(targetX), parent.width - width)) // max value } - y: atbLoader.valignment & Qt.AlignTop ? - -optionsToolBar.height + borderBg.height - : optionsToolBar.height - borderBg.height - state: if (SpectacleCore.annotationDocument.tool.options !== AnnotationTool.NoOptions - || (SpectacleCore.annotationDocument.tool.type === AnnotationTool.SelectTool - && SpectacleCore.annotationDocument.selectedItem.options !== AnnotationTool.NoOptions) + y: (annotationsToolBar.valignment & Qt.AlignTop) + && !SelectionEditor.selection.empty + ? -otbLoader.height + borderBg.height + : otbLoader.height - borderBg.height + state: if (root.document?.tool.options !== AnnotationTool.NoOptions + || (root.document?.tool.type === AnnotationTool.SelectTool + && root.document?.selectedItem.options !== AnnotationTool.NoOptions) ) { return "active" } else { @@ -579,13 +667,111 @@ MouseArea { sourceComponent: FloatingToolBar { focusPolicy: Qt.NoFocus contentItem: AnnotationOptionsToolBarContents { + spacing: parent.spacing displayMode: QQC.AbstractButton.IconOnly focusPolicy: Qt.NoFocus } - topLeftRadius: atbLoader.valignment & Qt.AlignBottom && x >= 0 ? 0 : radius - topRightRadius: atbLoader.valignment & Qt.AlignBottom && x + width <= annotationsToolBar.width ? 0 : radius - bottomLeftRadius: atbLoader.valignment & Qt.AlignTop && x >= 0 ? 0 : radius - bottomRightRadius: atbLoader.valignment & Qt.AlignTop && x + width <= annotationsToolBar.width ? 0 : radius + topLeftRadius: ((annotationsToolBar.valignment & Qt.AlignBottom) || SelectionEditor.selection.empty) && otbLoader.x >= 0 ? 0 : radius + topRightRadius: ((annotationsToolBar.valignment & Qt.AlignBottom) || SelectionEditor.selection.empty) && otbLoader.x + width <= annotationsToolBar.width ? 0 : radius + bottomLeftRadius: (annotationsToolBar.valignment & Qt.AlignTop) && !SelectionEditor.selection.empty && otbLoader.x >= 0 ? 0 : radius + bottomRightRadius: (annotationsToolBar.valignment & Qt.AlignTop) && !SelectionEditor.selection.empty && otbLoader.x + width <= annotationsToolBar.width ? 0 : radius + } + } + } + } + + // Finalizer ToolBar + Loader { + id: ftbLoader + readonly property rect rect: Qt.rect(x, y, width, height) + visible: !SpectacleCore.videoPlatform.isRecording && !SelectionEditor.selection.empty + active: visible + sourceComponent: FloatingToolBar { + id: toolBar + property bool rememberPosition: false + property alias dragging: dragHandler.active + readonly property int valignment: { + if (SelectionEditor.screensRect.height - SelectionEditor.handlesRect.bottom >= height + toolBar.topPadding || SelectionEditor.selection.empty) { + // the bottom of the bottom side of the selection + // or the bottom of the screen + return Qt.AlignBottom + } else { + // the top of the bottom side of the selection + return Qt.AlignTop + } + } + Binding { + property: "x" + target: ftbLoader + value: { + const v = SelectionEditor.selection.empty ? (root.width - toolBar.width) / 2 + root.viewportRect.x + : SelectionEditor.selection.horizontalCenter - toolBar.width / 2 + return Math.max(toolBar.leftPadding, // min value + Math.min(dprRound(v), + SelectionEditor.screensRect.width - toolBar.width - toolBar.rightPadding)) // max value + } + when: screensRectItem.allowToolbars && !toolBar.rememberPosition + restoreMode: Binding.RestoreNone + } + Binding { + property: "y" + target: ftbLoader + value: { + let v = 0 + if (SelectionEditor.selection.empty) { + v = root.viewportRect.y + root.height - toolBar.height - toolBar.bottomPadding + } else if (toolBar.valignment & Qt.AlignBottom) { + v = SelectionEditor.handlesRect.bottom + toolBar.topPadding + } else if (toolBar.valignment & Qt.AlignTop) { + v = Math.min(SelectionEditor.selection.bottom, SelectionEditor.handlesRect.bottom - Kirigami.Units.gridUnit) + - toolBar.height - toolBar.bottomPadding + } else { + v = (toolBar.height / 2) - toolBar.parent.y + } + return dprRound(v) + } + when: screensRectItem.allowToolbars && !toolBar.rememberPosition + restoreMode: Binding.RestoreNone + } + visible: opacity > 0 + opacity: screensRectItem.allowToolbars && Geometry.rectIntersects(ftbLoader.rect, root.viewportRect) + Behavior on opacity { + NumberAnimation { + duration: Kirigami.Units.longDuration + easing.type: Easing.OutCubic + } + } + layer.enabled: true // improves the visuals of the opacity animation + focusPolicy: Qt.NoFocus + contentItem: Loader { + sourceComponent: SpectacleCore.videoMode ? videoToolBarComponent : imageFinalizerToolBarComponent + } + + HoverHandler { + enabled: dragHandler.enabled + target: toolBar + margin: toolBar.padding + cursorShape: enabled ? + (dragHandler.active ? Qt.ClosedHandCursor : Qt.OpenHandCursor) + : undefined + } + DragHandler { // parent is contentItem and parent is a read-only property + id: dragHandler + enabled: SelectionEditor.selection.empty || !selectionRectangle.enabled + target: ftbLoader + acceptedButtons: Qt.LeftButton + margin: toolBar.padding + xAxis.minimum: root.viewportRect.x + xAxis.maximum: root.viewportRect.x + root.width - toolBar.width + yAxis.minimum: root.viewportRect.y + yAxis.maximum: root.viewportRect.y + root.height - toolBar.height + onActiveChanged: if (active) { + mainToolBar.z = mainToolBar.z > atbLoader.z ? 1 : 0 + atbLoader.z = mainToolBar.z < atbLoader.z ? 1 : 0 + ftbLoader.z = 2 + if (!toolBar.rememberPosition) { + toolBar.rememberPosition = true + } } } } diff --git a/src/Gui/ImageView.qml b/src/Gui/ImageView.qml index ba68e4ca..2be177ec 100644 --- a/src/Gui/ImageView.qml +++ b/src/Gui/ImageView.qml @@ -26,12 +26,11 @@ EmptyPage { readonly property real minimumWidth: Math.max( header.implicitWidth, annotationsToolBar.implicitWidth + separator.implicitWidth + footerLoader.implicitWidth, - captureOptionsLoader.implicitWidth + 480 // leave some room for content if necessary + 480 // leave some room for content if necessary ) readonly property real minimumHeight: header.implicitHeight + Math.max(annotationsToolBar.implicitHeight, - footerLoader.implicitHeight, - captureOptionsLoader.implicitHeight) + footerLoader.implicitHeight) property var inlineMessageData: {} property string inlineMessageSource: "" @@ -47,12 +46,46 @@ EmptyPage { header: QQC.ToolBar { id: header - contentItem: MainToolBarContents { + contentItem: ButtonGrid { id: mainToolBarContents - showNewScreenshotButton: false - showOptionsMenu: false - showUndoRedo: contextWindow.annotating - displayMode: QQC.AbstractButton.TextBesideIcon + animationsEnabled: true + AnimatedLoader { + state: contextWindow.annotating ? "active" : "inactive" + sourceComponent: UndoRedoGroup { + buttonHeight: QmlUtils.iconTextButtonHeight + spacing: mainToolBarContents.spacing + } + } + TtToolButton { + display: TtToolButton.IconOnly + visible: action.enabled + action: SaveAction {} + } + TtToolButton { + display: SpectacleCore.videoMode ? TtToolButton.TextBesideIcon : TtToolButton.IconOnly + action: SaveAsAction {} + } + TtToolButton { + display: TtToolButton.IconOnly + visible: action.enabled + action: CopyImageAction {} + } + // We only show this in video mode to save space in screenshot mode + TtToolButton { + visible: SpectacleCore.videoMode + action: CopyLocationAction {} + } + ExportMenuButton {} + TtToolButton { + visible: action.enabled + action: EditAction {} + } + QQC.ToolSeparator { + height: QmlUtils.iconTextButtonHeight + } + ScreenshotModeMenuButton {} + RecordingModeMenuButton {} + OptionsMenuButton {} } } @@ -64,7 +97,7 @@ EmptyPage { AnimatedLoader { // parent is contentItem id: inlineMessageLoader anchors.left: annotationsToolBar.right - anchors.right: captureOptionsLoader.left + anchors.right: parent.right anchors.top: parent.top state: "inactive" height: visible ? implicitHeight : 0 @@ -111,7 +144,7 @@ EmptyPage { id: contentLoader anchors { left: footerLoader.left - right: captureOptionsLoader.left + right: parent.right top: inlineMessageLoader.bottom bottom: footerLoader.top } @@ -126,78 +159,10 @@ EmptyPage { } } - Loader { // parent is contentItem - id: captureOptionsLoader - visible: true - active: visible - anchors { - top: parent.top - bottom: parent.bottom - right: parent.right - } - width: Math.max(implicitWidth, Kirigami.Units.gridUnit * 15) - sourceComponent: QQC.Page { - - leftPadding: Kirigami.Units.mediumSpacing * 2 - + (!mirrored ? sideBarSeparator.implicitWidth : 0) - rightPadding: Kirigami.Units.mediumSpacing * 2 - + (mirrored ? sideBarSeparator.implicitWidth : 0) - topPadding: Kirigami.Units.mediumSpacing * 2 - bottomPadding: Kirigami.Units.mediumSpacing * 2 - - header: RowLayout { - spacing: 0 - Kirigami.Separator { - Layout.fillHeight: true - } - Kirigami.NavigationTabBar { - id: tabBar - Layout.fillWidth: true - visible: SpectacleCore.videoPlatform.supportedRecordingModes - currentIndex: 0 - Kirigami.Theme.colorSet: Kirigami.Theme.Window - - actions: [ - Kirigami.Action { - text: i18n("Screenshot") - icon.name: "camera-photo" - checked: tabBar.currentIndex === 0 - }, - Kirigami.Action { - text: i18n("Recording") - icon.name: "camera-video" - checked: tabBar.currentIndex === 1 - } - ] - } - } - - contentItem: Loader { - source: switch (tabBar.currentIndex) { - case 0: return "CaptureOptions.qml" - case 1: return "RecordOptions.qml" - default: return "" - } - } - - background: Rectangle { - color: Kirigami.Theme.backgroundColor - Kirigami.Separator { - id: sideBarSeparator - anchors { - left: parent.left - top: parent.top - bottom: parent.bottom - } - } - } - } - } - Loader { id: footerLoader anchors.left: separator.right - anchors.right: captureOptionsLoader.left + anchors.right: parent.right anchors.top: parent.bottom visible: false active: visible @@ -294,11 +259,6 @@ EmptyPage { anchors.bottom: parent.bottom anchors.top: undefined } - AnchorChanges { - target: captureOptionsLoader - anchors.left: parent.right - anchors.right: undefined - } }, State { name: "normal" @@ -315,11 +275,6 @@ EmptyPage { anchors.bottom: undefined anchors.top: parent.bottom } - AnchorChanges { - target: captureOptionsLoader - anchors.left: undefined - anchors.right: parent.right - } } ] transitions: [ @@ -335,21 +290,11 @@ EmptyPage { duration: Kirigami.Units.longDuration easing.type: Easing.OutCubic } - PropertyAction { - targets: captureOptionsLoader - property: "visible" - value: false - } } }, Transition { to: "normal" SequentialAnimation { - PropertyAction { - targets: captureOptionsLoader - property: "visible" - value: true - } AnchorAnimation { duration: Kirigami.Units.longDuration easing.type: Easing.OutCubic diff --git a/src/Gui/MainToolBarContents.qml b/src/Gui/MainToolBarContents.qml deleted file mode 100644 index eaf2ad01..00000000 --- a/src/Gui/MainToolBarContents.qml +++ /dev/null @@ -1,153 +0,0 @@ -/* SPDX-FileCopyrightText: 2022 Noah Davis - * SPDX-License-Identifier: LGPL-2.0-or-later - */ - -import QtQuick -import QtQuick.Controls as QQC -import org.kde.kirigami as Kirigami -import org.kde.spectacle.private - -ButtonGrid { - id: root - property size imageSize: Qt.size(0, 0) - property bool showSizeLabel: false - property bool showUndoRedo: false - property bool showNewScreenshotButton: true - property bool showOptionsMenu: true - - component ToolButton: QQC.ToolButton { - implicitHeight: QmlUtils.iconTextButtonHeight - width: display === QQC.ToolButton.IconOnly ? height : implicitWidth - focusPolicy: root.focusPolicy - display: root.displayMode - QQC.ToolTip.text: text - QQC.ToolTip.visible: (hovered || pressed) && display === QQC.ToolButton.IconOnly - QQC.ToolTip.delay: Kirigami.Units.toolTipDelay - } - - AnimatedLoader { - id: sizeLabelLoader - state: root.showSizeLabel && root.imageSize.width > 0 && root.imageSize.height > 0 ? - "active" : "inactive" - sourceComponent: SizeLabel { - height: QmlUtils.iconTextButtonHeight - size: root.imageSize - leftPadding: Kirigami.Units.mediumSpacing + QmlUtils.fontMetrics.descent - rightPadding: leftPadding - } - } - - AnimatedLoader { - state: root.showUndoRedo ? "active" : "inactive" - sourceComponent: UndoRedoGroup { - animationsEnabled: root.animationsEnabled - buttonHeight: QmlUtils.iconTextButtonHeight - focusPolicy: root.focusPolicy - flow: root.flow - spacing: root.spacing - } - } - - // We don't show this in video mode because the video is already automatically saved. - // and you can't edit the video. - ToolButton { - visible: !SpectacleCore.videoMode - icon.name: "document-save" - text: i18n("Save") - onClicked: contextWindow.save() - } - - ToolButton { - icon.name: "document-save-as" - text: i18n("Save As...") - onClicked: contextWindow.saveAs() - } - - // We don't show this in video mode because you can't copy raw video to the clipboard, - // or at least not elegantly. - ToolButton { - visible: !SpectacleCore.videoMode - icon.name: "edit-copy" - text: i18n("Copy") - onClicked: contextWindow.copyImage() - } - - // We only show this in video mode to save space in screenshot mode - ToolButton { - visible: SpectacleCore.videoMode - icon.name: "edit-copy-path" - text: i18n("Copy Location") - onClicked: contextWindow.copyLocation() - } - - ToolButton { - // FIXME: make export menu actually work with videos - visible: !SpectacleCore.videoMode - icon.name: "document-share" - text: i18n("Export") - down: pressed || ExportMenu.visible - Accessible.role: Accessible.ButtonMenu - onPressed: ExportMenu.popup(this) - } - - ToolButton { - id: annotationsButton - icon.name: "edit-image" - text: i18nc("@action:button edit screenshot", "Edit…") - visible: !SpectacleCore.videoMode - checkable: true - checked: contextWindow.annotating - onToggled: contextWindow.annotating = checked - } - - ToolButton { - // Can't rely on checked since clicking also toggles checked - readonly property bool showCancel: SpectacleCore.captureTimeRemaining > 0 - readonly property real cancelWidth: QmlUtils.getButtonSize(display, cancelText(Settings.captureDelay), icon.name).width - - function cancelText(seconds) { - return i18np("Cancel (%1 second)", "Cancel (%1 seconds)", Math.ceil(seconds)) - } - - visible: root.showNewScreenshotButton - checked: showCancel - width: if (showCancel) { - return cancelWidth - } else { - return display === QQC.ToolButton.IconOnly ? height : implicitWidth - } - icon.name: showCancel ? "dialog-cancel" : "list-add" - text: showCancel ? - cancelText(SpectacleCore.captureTimeRemaining / 1000) - : i18n("New Screenshot") - onClicked: if (showCancel) { - SpectacleCore.cancelScreenshot() - } else { - SpectacleCore.takeNewScreenshot() - } - } - - ToolButton { - visible: root.showOptionsMenu - icon.name: "configure" - text: i18n("Options") - down: pressed || OptionsMenu.visible - Accessible.role: Accessible.ButtonMenu - onPressed: OptionsMenu.popup(this) - } - ToolButton { - visible: !root.showOptionsMenu - icon.name: "configure" - text: i18n("Configure...") - onClicked: OptionsMenu.showPreferencesDialog(); - } - - ToolButton { - id: helpButton - icon.name: "help-contents" - text: i18n("Help") - down: pressed || HelpMenu.visible - Accessible.role: Accessible.ButtonMenu - onPressed: HelpMenu.popup(this) - } -} diff --git a/src/Gui/NewScreenshotToolButton.qml b/src/Gui/NewScreenshotToolButton.qml new file mode 100644 index 00000000..4c7aa91a --- /dev/null +++ b/src/Gui/NewScreenshotToolButton.qml @@ -0,0 +1,33 @@ +/* SPDX-FileCopyrightText: 2025 Noah Davis + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.kirigami as Kirigami +import org.kde.spectacle.private + +TtToolButton { + // Can't rely on checked since clicking also toggles checked + readonly property bool showCancel: SpectacleCore.captureTimeRemaining > 0 + readonly property real cancelWidth: QmlUtils.getButtonSize(display, cancelText(Settings.captureDelay), icon.name).width + + function cancelText(seconds) { + return i18np("Cancel (%1 second)", "Cancel (%1 seconds)", Math.ceil(seconds)) + } + + checked: showCancel + width: if (showCancel) { + return cancelWidth + } else { + return display === TtToolButton.IconOnly ? height : implicitWidth + } + icon.name: showCancel ? "dialog-cancel" : "list-add" + text: showCancel ? + cancelText(SpectacleCore.captureTimeRemaining / 1000) + : i18n("New Screenshot") + onClicked: if (showCancel) { + SpectacleCore.cancelScreenshot() + } else { + SpectacleCore.takeNewScreenshot() + } +} diff --git a/src/Gui/OptionsMenu.cpp b/src/Gui/OptionsMenu.cpp index e031f8f1..d9ac01da 100644 --- a/src/Gui/OptionsMenu.cpp +++ b/src/Gui/OptionsMenu.cpp @@ -5,15 +5,21 @@ #include "OptionsMenu.h" #include "CaptureModeModel.h" +#include "Gui/SmartSpinBox.h" #include "Gui/SettingsDialog/SettingsDialog.h" #include "SpectacleCore.h" #include "WidgetWindowUtils.h" +#include "HelpMenu.h" #include "settings.h" #include #include +#include +#include +#include #include +#include using namespace Qt::StringLiterals; @@ -21,158 +27,129 @@ static QPointer s_instance = nullptr; OptionsMenu::OptionsMenu(QWidget *parent) : SpectacleMenu(parent) - , captureModeSection(new QAction(this)) - , captureModeGroup(new QActionGroup(this)) // exclusive by default) - , captureSettingsSection(new QAction(this)) - , includeMousePointerAction(new QAction(this)) - , includeWindowDecorationsAction(new QAction(this)) - , includeWindowShadowAction(new QAction(this)) - , onlyCapturePopupAction(new QAction(this)) - , quitAfterSaveAction(new QAction(this)) - , captureOnClickAction(new QAction(this)) - , delayAction(new QWidgetAction(this)) - , delayWidget(new QWidget(this)) - , delayLayout(new QHBoxLayout(delayWidget.get())) - , delayLabel(new QLabel(delayWidget.get())) - , delaySpinBox(new SmartSpinBox(delayWidget.get())) + , m_delayAction(new QWidgetAction(this)) + , m_delayWidget(new QWidget(this)) + , m_delayLayout(new QHBoxLayout(m_delayWidget.get())) + , m_delayLabel(new QLabel(m_delayWidget.get())) + , m_delaySpinBox(new SmartSpinBox(m_delayWidget.get())) { - addAction(KStandardActions::preferences(this, &OptionsMenu::showPreferencesDialog, this)); - + setToolTipsVisible(true); // QMenu::addSection just adds an action with text and separator mode enabled - captureModeSection->setText(i18n("Capture Mode")); - captureModeSection->setSeparator(true); - addAction(captureModeSection.get()); + addSection(i18nc("@title:menu", "Screenshot Settings")); - // Add capture mode actions. - // This cannot be done in the constructor because captureModeModel will be null at this time. - connect(this, &OptionsMenu::aboutToShow, - this, &OptionsMenu::updateCaptureModes); - - // make capture mode actions do things - connect(captureModeGroup.get(), &QActionGroup::triggered, this, [](QAction *action){ - int mode = action->data().toInt(); - Settings::setCaptureMode(mode); - }); - connect(Settings::self(), &Settings::captureModeChanged, this, [this](){ - int mode = Settings::captureMode(); - if (captureModeGroup->checkedAction() && mode == captureModeGroup->checkedAction()->data().toInt()) { - return; - } - for (auto action : std::as_const(captureModeActions)) { - if (mode == action->data().toInt()) { - action->setChecked(true); - } - } - }); - - captureSettingsSection->setText(i18n("Capture Settings")); - captureSettingsSection->setSeparator(true); - addAction(captureSettingsSection.get()); - - includeMousePointerAction->setText(i18n("Include mouse pointer")); - includeMousePointerAction->setToolTip(i18n("Show the mouse cursor in the screenshot image")); + auto includeMousePointerAction = addAction(i18nc("@option:check for screenshots", "Include mouse pointer")); + includeMousePointerAction->setToolTip(i18nc("@info:tooltip", "Show the mouse cursor in the screenshot image")); includeMousePointerAction->setCheckable(true); includeMousePointerAction->setChecked(Settings::includePointer()); - connect(includeMousePointerAction.get(), &QAction::toggled, this, [](bool checked){ - Settings::setIncludePointer(checked); - }); - connect(Settings::self(), &Settings::includePointerChanged, this, [this](){ + QObject::connect(includeMousePointerAction, &QAction::toggled, Settings::self(), &Settings::setIncludePointer); + QObject::connect(Settings::self(), &Settings::includePointerChanged, includeMousePointerAction, [includeMousePointerAction](){ includeMousePointerAction->setChecked(Settings::includePointer()); }); - addAction(includeMousePointerAction.get()); - includeWindowDecorationsAction->setText(i18n("Include window titlebar and borders")); - includeWindowDecorationsAction->setToolTip(i18n("Show the window title bar, the minimize/maximize/close buttons, and the window border")); + auto includeWindowDecorationsAction = addAction(i18nc("@option:check", "Include window titlebar and borders")); + includeWindowDecorationsAction->setToolTip(i18nc("@info:tooltip", "Show the window title bar, the minimize/maximize/close buttons, and the window border")); includeWindowDecorationsAction->setCheckable(true); includeWindowDecorationsAction->setChecked(Settings::includeDecorations()); - connect(includeWindowDecorationsAction.get(), &QAction::toggled, this, [](bool checked){ - Settings::setIncludeDecorations(checked); - }); - connect(Settings::self(), &Settings::includeDecorationsChanged, this, [this](){ + QObject::connect(includeWindowDecorationsAction, &QAction::toggled, Settings::self(), Settings::setIncludeDecorations); + QObject::connect(Settings::self(), &Settings::includeDecorationsChanged, includeWindowDecorationsAction, [includeWindowDecorationsAction](){ includeWindowDecorationsAction->setChecked(Settings::includeDecorations()); }); - addAction(includeWindowDecorationsAction.get()); - includeWindowShadowAction->setText(i18n("Include window shadow")); - includeWindowShadowAction->setToolTip(i18n("Show the window shadow")); + auto includeWindowShadowAction = addAction(i18nc("@option:check", "Include window shadow")); + includeWindowShadowAction->setToolTip(i18nc("@info:tooltip", "Show the window shadow")); includeWindowShadowAction->setCheckable(true); includeWindowShadowAction->setChecked(Settings::includeShadow()); - connect(includeWindowShadowAction.get(), &QAction::toggled, this, [](bool checked) { - Settings::setIncludeShadow(checked); - }); - connect(Settings::self(), &Settings::includeShadowChanged, this, [this]() { + QObject::connect(includeWindowShadowAction, &QAction::toggled, Settings::self(), &Settings::setIncludeShadow); + QObject::connect(Settings::self(), &Settings::includeShadowChanged, includeWindowShadowAction, [includeWindowShadowAction]() { includeWindowShadowAction->setChecked(Settings::includeShadow()); }); - addAction(includeWindowShadowAction.get()); - onlyCapturePopupAction->setText(i18n("Capture the current pop-up only")); - onlyCapturePopupAction->setToolTip( - i18n("Capture only the current pop-up window (like a menu, tooltip etc).\n" - "If disabled, the pop-up is captured along with the parent window")); - onlyCapturePopupAction->setCheckable(true); - onlyCapturePopupAction->setChecked(Settings::transientOnly()); - connect(onlyCapturePopupAction.get(), &QAction::toggled, this, [](bool checked){ - Settings::setTransientOnly(checked); - }); - connect(Settings::self(), &Settings::transientOnlyChanged, this, [this](){ + const bool hasTransientWithParent = SpectacleCore::instance()->imagePlatform()->supportedGrabModes().testFlag(ImagePlatform::TransientWithParent); + if (hasTransientWithParent) { + auto onlyCapturePopupAction = addAction(i18nc("@option:check", "Capture the current pop-up only")); + onlyCapturePopupAction->setToolTip( + i18nc("@info:tooltip", "Capture only the current pop-up window (like a menu, tooltip etc).\n" + "If disabled, the pop-up is captured along with the parent window")); + onlyCapturePopupAction->setCheckable(true); onlyCapturePopupAction->setChecked(Settings::transientOnly()); - }); - addAction(onlyCapturePopupAction.get()); + QObject::connect(onlyCapturePopupAction, &QAction::toggled, Settings::self(), &Settings::setTransientOnly); + QObject::connect(Settings::self(), &Settings::transientOnlyChanged, onlyCapturePopupAction, [onlyCapturePopupAction](){ + onlyCapturePopupAction->setChecked(Settings::transientOnly()); + }); + } - quitAfterSaveAction->setText(i18n("Quit after manual Save or Copy")); - quitAfterSaveAction->setToolTip(i18n("Quit Spectacle after manually saving or copying the image")); + auto quitAfterSaveAction = addAction(i18nc("@option:check", "Quit after manual Save or Copy")); + quitAfterSaveAction->setToolTip(i18nc("@info:tooltip", "Quit Spectacle after manually saving or copying the image")); quitAfterSaveAction->setCheckable(true); quitAfterSaveAction->setChecked(Settings::quitAfterSaveCopyExport()); - connect(quitAfterSaveAction.get(), &QAction::toggled, this, [](bool checked){ - Settings::setQuitAfterSaveCopyExport(checked); - }); - connect(Settings::self(), &Settings::quitAfterSaveCopyExportChanged, this, [this](){ + QObject::connect(quitAfterSaveAction, &QAction::toggled, Settings::self(), &Settings::setQuitAfterSaveCopyExport); + QObject::connect(Settings::self(), &Settings::quitAfterSaveCopyExportChanged, quitAfterSaveAction, [quitAfterSaveAction](){ quitAfterSaveAction->setChecked(Settings::quitAfterSaveCopyExport()); }); - addAction(quitAfterSaveAction.get()); // add capture on click const bool hasOnClick = SpectacleCore::instance()->imagePlatform()->supportedShutterModes().testFlag(ImagePlatform::OnClick); - addSeparator()->setVisible(hasOnClick); - captureOnClickAction->setText(i18n("Capture On Click")); - captureOnClickAction->setCheckable(true); - captureOnClickAction->setChecked(Settings::captureOnClick() && hasOnClick); - captureOnClickAction->setVisible(hasOnClick); - connect(captureOnClickAction.get(), &QAction::toggled, this, [this](bool checked){ - Settings::setCaptureOnClick(checked); - delayAction->setEnabled(!checked); - }); - connect(Settings::self(), &Settings::captureOnClickChanged, this, [this](){ + if (hasOnClick) { + addSeparator(); + auto captureOnClickAction = addAction(i18nc("@option:check", "Capture On Click")); + captureOnClickAction->setCheckable(true); captureOnClickAction->setChecked(Settings::captureOnClick()); - }); - addAction(captureOnClickAction.get()); + QObject::connect(captureOnClickAction, &QAction::toggled, this, [this](bool checked){ + Settings::setCaptureOnClick(checked); + m_delayAction->setEnabled(!checked); + }); + QObject::connect(Settings::self(), &Settings::captureOnClickChanged, captureOnClickAction, [captureOnClickAction](){ + captureOnClickAction->setChecked(Settings::captureOnClick()); + }); + } // set up delay widget - auto spinbox = delaySpinBox.get(); - auto label = delayLabel.get(); - label->setText(i18n("Delay:")); + auto spinbox = m_delaySpinBox.get(); + auto label = m_delayLabel.get(); + label->setText(i18nc("@label:spinbox", "Delay:")); spinbox->setDecimals(1); spinbox->setSingleStep(1.0); spinbox->setMinimum(0.0); spinbox->setMaximum(999); - spinbox->setSpecialValueText(i18n("No Delay")); + spinbox->setSpecialValueText(i18nc("@item 0 delay special value", "No Delay")); delayActionLayoutUpdate(); - connect(spinbox, qOverload(&SmartSpinBox::valueChanged), this, [this](){ - if (updatingDelayActionLayout) { + QObject::connect(spinbox, qOverload(&SmartSpinBox::valueChanged), this, [this](){ + if (m_updatingDelayActionLayout) { return; } - Settings::setCaptureDelay(delaySpinBox->value()); + Settings::setCaptureDelay(m_delaySpinBox->value()); + }); + QObject::connect(Settings::self(), &Settings::captureDelayChanged, spinbox, [this](){ + m_delaySpinBox->setValue(Settings::captureDelay()); }); - connect(Settings::self(), &Settings::captureDelayChanged, this, [this](){ - delaySpinBox->setValue(Settings::captureDelay()); + m_delayWidget->setLayout(m_delayLayout.get()); + m_delayLayout->addWidget(label); + m_delayLayout->addWidget(spinbox); + m_delayLayout->setAlignment(Qt::AlignLeft); + m_delayAction->setDefaultWidget(m_delayWidget.get()); + m_delayAction->setEnabled(!hasOnClick || !Settings::captureOnClick()); + addAction(m_delayAction.get()); + + addSection(i18nc("@title:menu", "Recording Settings")); + + auto videoIncludeMousePointerAction = addAction(i18nc("@option:check for recordings", "Include mouse pointer")); + videoIncludeMousePointerAction->setToolTip(i18nc("@info:tooltip", "Show the mouse cursor in the recording")); + videoIncludeMousePointerAction->setCheckable(true); + videoIncludeMousePointerAction->setChecked(Settings::videoIncludePointer()); + QObject::connect(videoIncludeMousePointerAction, &QAction::toggled, Settings::self(), &Settings::setVideoIncludePointer); + QObject::connect(Settings::self(), &Settings::videoIncludePointerChanged, videoIncludeMousePointerAction, [videoIncludeMousePointerAction](){ + videoIncludeMousePointerAction->setChecked(Settings::videoIncludePointer()); }); - delayWidget->setLayout(delayLayout.get()); - delayLayout->addWidget(label); - delayLayout->addWidget(spinbox); - delayLayout->setAlignment(Qt::AlignLeft); - delayAction->setDefaultWidget(delayWidget.get()); - delayAction->setEnabled(!captureOnClickAction->isChecked()); - addAction(delayAction.get()); + + addSeparator(); + + addAction(KStandardActions::preferences(this, &OptionsMenu::showPreferencesDialog, this)); + + addMenu(HelpMenu::instance()); + connect(this, &OptionsMenu::aboutToShow, + this, [this] { + setWidgetTransientParentToWidget(HelpMenu::instance(), this); + }); } OptionsMenu *OptionsMenu::instance() @@ -201,9 +178,26 @@ void OptionsMenu::showPreferencesDialog() dialog->show(); } -void OptionsMenu::setCaptureModeOptionsEnabled(bool enabled) +void OptionsMenu::delayActionLayoutUpdate() { - captureModeOptionsEnabled = enabled; + // We can't block signals while doing this to prevent unnecessary + // processing because the spinbox has internal connections that need + // to work in order to get the correct size. + // We use our own guarding variable instead. + m_updatingDelayActionLayout = true; + m_delaySpinBox->setValue(m_delaySpinBox->maximum()); + m_delaySpinBox->setMinimumWidth(m_delaySpinBox->sizeHint().width()); + m_delaySpinBox->setValue(Settings::captureDelay()); + m_updatingDelayActionLayout = false; + + int menuHMargin = style()->pixelMetric(QStyle::PM_MenuHMargin); + int menuVMargin = style()->pixelMetric(QStyle::PM_MenuVMargin); + if (layoutDirection() == Qt::RightToLeft) { + m_delayLabel->setContentsMargins(0, 0, menuHMargin + m_delayLabel->fontMetrics().descent(), 0); + } else { + m_delayLabel->setContentsMargins(menuHMargin + m_delayLabel->fontMetrics().descent(), 0, 0, 0); + } + m_delayLayout->setContentsMargins(0, menuVMargin, 0, 0); } void OptionsMenu::changeEvent(QEvent *event) @@ -219,80 +213,30 @@ void OptionsMenu::changeEvent(QEvent *event) QWidget::changeEvent(event); } -void OptionsMenu::delayActionLayoutUpdate() +void OptionsMenu::keyPressEvent(QKeyEvent *event) { - // We can't block signals while doing this to prevent unnecessary - // processing because the spinbox has internal connections that need - // to work in order to get the correct size. - // We use our own guarding variable instead. - updatingDelayActionLayout = true; - delaySpinBox->setValue(delaySpinBox->maximum()); - delaySpinBox->setMinimumWidth(delaySpinBox->sizeHint().width()); - delaySpinBox->setValue(Settings::captureDelay()); - updatingDelayActionLayout = false; - - int menuHMargin = style()->pixelMetric(QStyle::PM_MenuHMargin); - int menuVMargin = style()->pixelMetric(QStyle::PM_MenuVMargin); - if (layoutDirection() == Qt::RightToLeft) { - delayLabel->setContentsMargins(0, 0, menuHMargin + delayLabel->fontMetrics().descent(), 0); - } else { - delayLabel->setContentsMargins(menuHMargin + delayLabel->fontMetrics().descent(), 0, 0, 0); + // Try to keep menu open when triggering checkable actions + const auto key = event->key(); + const auto action = activeAction(); + if (action && action->isEnabled() && action->isCheckable() // + && (key == Qt::Key_Return || key == Qt::Key_Enter // + || (key == Qt::Key_Space && style()->styleHint(QStyle::SH_Menu_SpaceActivatesItem, nullptr, this)))) { + action->trigger(); + event->accept(); + return; } - delayLayout->setContentsMargins(0, menuVMargin, 0, 0); + SpectacleMenu::keyPressEvent(event); } -void OptionsMenu::updateCaptureModes() +void OptionsMenu::mouseReleaseEvent(QMouseEvent *event) { - captureModeSection->setVisible(captureModeOptionsEnabled); - if (!captureModeOptionsEnabled) { - for (auto action : std::as_const(captureModeActions)) { - captureModeGroup->removeAction(action); - removeAction(action); - action->deleteLater(); - } - captureModeActions.clear(); - return; - } - - if (!captureModeModel) { - captureModeModel = std::make_unique(); - } - - // Only make this conneciton once. - // Can't be done in the constructor because captureModeModel is null at that time. - if (!captureModesInitialized) { - connect(captureModeModel.get(), &CaptureModeModel::captureModesChanged, this, [this]() { - shouldUpdateCaptureModes = true; - }); - captureModesInitialized = true; - } - // avoid unnecessarily resetting actions - if (!shouldUpdateCaptureModes) { + // Try to keep menu open when triggering checkable actions + const auto action = activeAction() == actionAt(event->position().toPoint()) ? activeAction() : nullptr; + if (action && action->isEnabled() && action->isCheckable()) { + action->trigger(); return; } - shouldUpdateCaptureModes = false; - for (auto action : std::as_const(captureModeActions)) { - captureModeGroup->removeAction(action); - removeAction(action); - action->deleteLater(); - } - captureModeActions.clear(); - for (int i = 0; i < captureModeModel->rowCount(); ++i) { - auto index = captureModeModel->index(i); - auto action = new QAction(this); - captureModeActions.append(action); - action->setText(captureModeModel->data(index, Qt::DisplayRole).toString()); - const auto mode = captureModeModel->data(index, CaptureModeModel::CaptureModeRole).toInt(); - action->setData(mode); - action->setCheckable(true); - if (!CaptureWindow::instances().empty() && !SpectacleCore::instance()->videoMode()) { - action->setChecked(mode == CaptureModeModel::RectangularRegion); - } else if (mode == Settings::captureMode()) { - action->setChecked(true); - } - captureModeGroup->addAction(action); - insertAction(captureSettingsSection.get(), action); - } + SpectacleMenu::mouseReleaseEvent(event); } #include "moc_OptionsMenu.cpp" diff --git a/src/Gui/OptionsMenu.h b/src/Gui/OptionsMenu.h index 5f87d097..b600f3f4 100644 --- a/src/Gui/OptionsMenu.h +++ b/src/Gui/OptionsMenu.h @@ -6,20 +6,13 @@ #define OPTIONSMENU_H #include "SpectacleMenu.h" - #include "Gui/SmartSpinBox.h" -#include #include #include -#include #include #include -#include - -class CaptureModeModel; - /** * A menu that allows choosing capture modes and related options. */ @@ -34,8 +27,6 @@ public: Q_SLOT void showPreferencesDialog(); - void setCaptureModeOptionsEnabled(bool enabled); - static OptionsMenu *create(QQmlEngine *engine, QJSEngine *) { auto inst = instance(); @@ -47,37 +38,18 @@ public: protected: void changeEvent(QEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; -private: explicit OptionsMenu(QWidget *parent = nullptr); void delayActionLayoutUpdate(); - Q_SLOT void updateCaptureModes(); - - QList captureModeActions; - const std::unique_ptr captureModeSection; - const std::unique_ptr captureModeGroup; - const std::unique_ptr captureSettingsSection; - const std::unique_ptr includeMousePointerAction; - const std::unique_ptr includeWindowDecorationsAction; - const std::unique_ptr includeWindowShadowAction; - const std::unique_ptr onlyCapturePopupAction; - const std::unique_ptr quitAfterSaveAction; - const std::unique_ptr captureOnClickAction; - const std::unique_ptr delayAction; - const std::unique_ptr delayWidget; - const std::unique_ptr delayLayout; - const std::unique_ptr delayLabel; - const std::unique_ptr delaySpinBox; - - std::unique_ptr captureModeModel; - - bool captureModesInitialized = false; - bool shouldUpdateCaptureModes = true; - bool updatingDelayActionLayout = false; - bool captureModeOptionsEnabled = true; - - friend class OptionsMenuSingleton; + const std::unique_ptr m_delayAction; + const std::unique_ptr m_delayWidget; + const std::unique_ptr m_delayLayout; + const std::unique_ptr m_delayLabel; + const std::unique_ptr m_delaySpinBox; + bool m_updatingDelayActionLayout = false; }; #endif // OPTIONSMENU_H diff --git a/src/Gui/OptionsMenuButton.qml b/src/Gui/OptionsMenuButton.qml new file mode 100644 index 00000000..5a86ff96 --- /dev/null +++ b/src/Gui/OptionsMenuButton.qml @@ -0,0 +1,14 @@ +/* SPDX-FileCopyrightText: 2025 Noah Davis + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import org.kde.spectacle.private + +TtToolButton { + icon.name: "configure" + text: i18nc("@action", "Options") + down: pressed || OptionsMenu.visible + Accessible.role: Accessible.ButtonMenu + onPressed: OptionsMenu.popup(this) +} diff --git a/src/Gui/Outline.qml b/src/Gui/Outline.qml index 158e356f..85d9f6ca 100644 --- a/src/Gui/Outline.qml +++ b/src/Gui/Outline.qml @@ -17,6 +17,7 @@ Shape { property alias joinStyle: shapePath.joinStyle property alias svgPath: pathSvg.path property alias pathScale: shapePath.scale + property alias pathHints: shapePath.pathHints // Get a rectangular SVG path function rectanglePath(x, y, w, h) { diff --git a/src/Gui/RecordAction.qml b/src/Gui/RecordAction.qml new file mode 100644 index 00000000..7c7a83df --- /dev/null +++ b/src/Gui/RecordAction.qml @@ -0,0 +1,13 @@ +/* SPDX-FileCopyrightText: 2025 Noah Davis + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick.Templates as T +import org.kde.spectacle.private + +T.Action { + enabled: SpectacleCore.videoMode + icon.name: "media-record" + text: i18nc("@action start recording", "Record") + onTriggered: contextWindow.accept() +} diff --git a/src/Gui/RecordingModeButtonsColumn.qml b/src/Gui/RecordingModeButtonsColumn.qml index 58644a97..2737f620 100644 --- a/src/Gui/RecordingModeButtonsColumn.qml +++ b/src/Gui/RecordingModeButtonsColumn.qml @@ -12,7 +12,7 @@ import org.kde.spectacle.private ColumnLayout { spacing: Kirigami.Units.mediumSpacing Repeater { - model: RecordingModeModel { } + model: RecordingModeModel delegate: QQC.Button { id: button Layout.fillWidth: true diff --git a/src/Gui/RecordingModeMenu.cpp b/src/Gui/RecordingModeMenu.cpp new file mode 100644 index 00000000..d7451fa7 --- /dev/null +++ b/src/Gui/RecordingModeMenu.cpp @@ -0,0 +1,65 @@ +/* SPDX-FileCopyrightText: 2025 Noah Davis + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "RecordingModeMenu.h" +#include "RecordingModeModel.h" +#include "SpectacleCore.h" +#include "ShortcutActions.h" +#include + +using namespace Qt::StringLiterals; + +static QPointer s_instance = nullptr; + +RecordingModeMenu::RecordingModeMenu(QWidget *parent) + : SpectacleMenu(i18nc("@title:menu", "Recording Modes"), parent) +{ + auto addModes = [this] { + clear(); + auto model = RecordingModeModel::instance(); + for (auto idx = model->index(0); idx.isValid(); idx = idx.siblingAtRow(idx.row() + 1)) { + const auto action = addAction(idx.data(Qt::DisplayRole).toString()); + const auto mode = idx.data(RecordingModeModel::RecordingModeRole).value(); + QAction *globalAction = nullptr; + auto globalShortcuts = [](QAction *globalAction) { + if (!globalAction) { + return QList{}; + } + auto component = ShortcutActions::self()->componentName(); + auto id = globalAction->objectName(); + return KGlobalAccel::self()->globalShortcut(component, id); + }; + switch (mode) { + case VideoPlatform::Region: + globalAction = ShortcutActions::self()->recordRegionAction(); + break; + case VideoPlatform::Screen: + globalAction = ShortcutActions::self()->recordScreenAction(); + break; + case VideoPlatform::Window: + globalAction = ShortcutActions::self()->recordWindowAction(); + break; + default: + break; + } + action->setShortcuts(globalShortcuts(globalAction)); + auto onTriggered = [mode] { + SpectacleCore::instance()->startRecording(mode); + }; + connect(action, &QAction::triggered, action, onTriggered); + } + }; + addModes(); + connect(RecordingModeModel::instance(), &RecordingModeModel::recordingModesChanged, this, addModes); +} + +RecordingModeMenu *RecordingModeMenu::instance() +{ + if (!s_instance) { + s_instance = new RecordingModeMenu; + } + return s_instance; +} + +#include "moc_RecordingModeMenu.cpp" diff --git a/src/Gui/RecordingModeMenu.h b/src/Gui/RecordingModeMenu.h new file mode 100644 index 00000000..fe8f0a71 --- /dev/null +++ b/src/Gui/RecordingModeMenu.h @@ -0,0 +1,30 @@ +/* SPDX-FileCopyrightText: 2025 Noah Davis + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include "SpectacleMenu.h" +#include + +class RecordingModeMenu : public SpectacleMenu +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + +public: + static RecordingModeMenu *instance(); + + static RecordingModeMenu *create(QQmlEngine *engine, QJSEngine *) + { + auto inst = instance(); + Q_ASSERT(inst); + Q_ASSERT(inst->thread() == engine->thread()); + QJSEngine::setObjectOwnership(inst, QJSEngine::CppOwnership); + return inst; + } + +private: + explicit RecordingModeMenu(QWidget *parent = nullptr); +}; diff --git a/src/Gui/RecordingModeMenuButton.qml b/src/Gui/RecordingModeMenuButton.qml new file mode 100644 index 00000000..54d40666 --- /dev/null +++ b/src/Gui/RecordingModeMenuButton.qml @@ -0,0 +1,15 @@ +/* SPDX-FileCopyrightText: 2025 Noah Davis + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC +import org.kde.spectacle.private + +TtToolButton { + icon.name: "camera-video" + text: i18nc("@action select new recording mode", "New Recording") + down: pressed || RecordingModeMenu.visible + Accessible.role: Accessible.ButtonMenu + onPressed: RecordingModeMenu.popup(this) +} diff --git a/src/Gui/RecordingView.qml b/src/Gui/RecordingView.qml index fc7b6040..a4e747ae 100644 --- a/src/Gui/RecordingView.qml +++ b/src/Gui/RecordingView.qml @@ -87,13 +87,6 @@ FocusScope { id: tbHoverHandler } - component ToolButton: QQC.ToolButton { - display: QQC.ToolButton.IconOnly - QQC.ToolTip.text: text - QQC.ToolTip.visible: (hovered || pressed) && display === QQC.ToolButton.IconOnly - QQC.ToolTip.delay: Kirigami.Units.toolTipDelay - } - FloatingToolBar { id: toolBar anchors.left: parent.left @@ -105,7 +98,7 @@ FocusScope { enabled: root.hasContent contentItem: RowLayout { spacing: parent.spacing - ToolButton { + TtToolButton { id: playPauseButton containmentMask: Item { parent: playPauseButton diff --git a/src/Gui/SaveAction.qml b/src/Gui/SaveAction.qml new file mode 100644 index 00000000..084bda25 --- /dev/null +++ b/src/Gui/SaveAction.qml @@ -0,0 +1,15 @@ +/* SPDX-FileCopyrightText: 2025 Noah Davis + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick.Templates as T +import org.kde.spectacle.private + +T.Action { + // We don't use this in video mode because the video is already + // automatically saved and you can't edit the video. + enabled: !SpectacleCore.videoMode + icon.name: "document-save" + text: i18nc("@action", "Save") + onTriggered: contextWindow.save() +} diff --git a/src/Gui/SaveAsAction.qml b/src/Gui/SaveAsAction.qml new file mode 100644 index 00000000..371dfeca --- /dev/null +++ b/src/Gui/SaveAsAction.qml @@ -0,0 +1,11 @@ +/* SPDX-FileCopyrightText: 2025 Noah Davis + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick.Templates as T + +T.Action { + icon.name: "document-save-as" + text: i18nc("@action", "Save As…") + onTriggered: contextWindow.saveAs() +} diff --git a/src/Gui/ScreenshotModeMenu.cpp b/src/Gui/ScreenshotModeMenu.cpp new file mode 100644 index 00000000..341ae7ec --- /dev/null +++ b/src/Gui/ScreenshotModeMenu.cpp @@ -0,0 +1,74 @@ +/* SPDX-FileCopyrightText: 2025 Noah Davis + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "ScreenshotModeMenu.h" +#include "CaptureModeModel.h" +#include "SpectacleCore.h" +#include "ShortcutActions.h" +#include + +using namespace Qt::StringLiterals; + +static QPointer s_instance = nullptr; + +ScreenshotModeMenu::ScreenshotModeMenu(QWidget *parent) + : SpectacleMenu(i18nc("@title:menu", "Screenshot Modes"), parent) +{ + auto addModes = [this] { + clear(); + auto model = CaptureModeModel::instance(); + for (auto idx = model->index(0); idx.isValid(); idx = idx.siblingAtRow(idx.row() + 1)) { + const auto action = addAction(idx.data(Qt::DisplayRole).toString()); + const auto mode = idx.data(CaptureModeModel::CaptureModeRole).value(); + QAction *globalAction = nullptr; + auto globalShortcuts = [](QAction *globalAction) { + if (!globalAction) { + return QList{}; + } + auto component = ShortcutActions::self()->componentName(); + auto id = globalAction->objectName(); + return KGlobalAccel::self()->globalShortcut(component, id); + }; + switch (mode) { + case CaptureModeModel::RectangularRegion: + globalAction = ShortcutActions::self()->regionAction(); + break; + case CaptureModeModel::AllScreens: + globalAction = ShortcutActions::self()->fullScreenAction(); + break; + case CaptureModeModel::CurrentScreen: + globalAction = ShortcutActions::self()->currentScreenAction(); + break; + case CaptureModeModel::ActiveWindow: + globalAction = ShortcutActions::self()->activeWindowAction(); + break; + case CaptureModeModel::WindowUnderCursor: + globalAction = ShortcutActions::self()->windowUnderCursorAction(); + break; + case CaptureModeModel::FullScreen: + globalAction = ShortcutActions::self()->fullScreenAction(); + break; + default: + break; + } + action->setShortcuts(globalShortcuts(globalAction)); + auto onTriggered = [mode] { + SpectacleCore::instance()->takeNewScreenshot(mode); + }; + connect(action, &QAction::triggered, action, onTriggered); + } + }; + addModes(); + connect(CaptureModeModel::instance(), &CaptureModeModel::captureModesChanged, this, addModes); +} + +ScreenshotModeMenu *ScreenshotModeMenu::instance() +{ + if (!s_instance) { + s_instance = new ScreenshotModeMenu; + } + return s_instance; +} + +#include "moc_ScreenshotModeMenu.cpp" diff --git a/src/Gui/ScreenshotModeMenu.h b/src/Gui/ScreenshotModeMenu.h new file mode 100644 index 00000000..387a05db --- /dev/null +++ b/src/Gui/ScreenshotModeMenu.h @@ -0,0 +1,30 @@ +/* SPDX-FileCopyrightText: 2025 Noah Davis + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include "SpectacleMenu.h" +#include + +class ScreenshotModeMenu : public SpectacleMenu +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + +public: + static ScreenshotModeMenu *instance(); + + static ScreenshotModeMenu *create(QQmlEngine *engine, QJSEngine *) + { + auto inst = instance(); + Q_ASSERT(inst); + Q_ASSERT(inst->thread() == engine->thread()); + QJSEngine::setObjectOwnership(inst, QJSEngine::CppOwnership); + return inst; + } + +private: + explicit ScreenshotModeMenu(QWidget *parent = nullptr); +}; diff --git a/src/Gui/ScreenshotModeMenuButton.qml b/src/Gui/ScreenshotModeMenuButton.qml new file mode 100644 index 00000000..5440f17d --- /dev/null +++ b/src/Gui/ScreenshotModeMenuButton.qml @@ -0,0 +1,15 @@ +/* SPDX-FileCopyrightText: 2025 Noah Davis + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC +import org.kde.spectacle.private + +TtToolButton { + icon.name: "camera-photo" + text: i18nc("@action select new screenshot mode", "New Screenshot") + down: pressed || ScreenshotModeMenu.visible + Accessible.role: Accessible.ButtonMenu + onPressed: ScreenshotModeMenu.popup(this) +} diff --git a/src/Gui/SettingsDialog/GeneralOptions.ui b/src/Gui/SettingsDialog/GeneralOptions.ui index 474ae28d..b7d6ddb5 100644 --- a/src/Gui/SettingsDialog/GeneralOptions.ui +++ b/src/Gui/SettingsDialog/GeneralOptions.ui @@ -20,6 +20,11 @@ + + + Take rectangular screenshot + + Take full screen screenshot diff --git a/src/Gui/SettingsDialog/spectacle.kcfg b/src/Gui/SettingsDialog/spectacle.kcfg index 3d916aee..f58f28fc 100644 --- a/src/Gui/SettingsDialog/spectacle.kcfg +++ b/src/Gui/SettingsDialog/spectacle.kcfg @@ -15,11 +15,12 @@ + - TakeFullscreenScreenshot + TakeRectangularScreenshot diff --git a/src/Gui/TtToolButton.qml b/src/Gui/TtToolButton.qml new file mode 100644 index 00000000..31de1309 --- /dev/null +++ b/src/Gui/TtToolButton.qml @@ -0,0 +1,16 @@ +/* SPDX-FileCopyrightText: 2025 Noah Davis + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls as QQC +import org.kde.kirigami as Kirigami +import org.kde.spectacle.private + +QQC.ToolButton { + implicitHeight: QmlUtils.iconTextButtonHeight + width: display === QQC.ToolButton.IconOnly ? height : implicitWidth + QQC.ToolTip.text: text + QQC.ToolTip.visible: (hovered || pressed) && display === QQC.ToolButton.IconOnly + QQC.ToolTip.delay: Kirigami.Units.toolTipDelay +} diff --git a/src/Gui/UndoRedoGroup.qml b/src/Gui/UndoRedoGroup.qml index 83fa44fa..19deb798 100644 --- a/src/Gui/UndoRedoGroup.qml +++ b/src/Gui/UndoRedoGroup.qml @@ -22,7 +22,7 @@ Grid { NumberAnimation { properties: "x,y"; duration: Kirigami.Units.longDuration; easing.type: Easing.OutCubic } } - QQC.ToolButton { + TtToolButton { id: undoButton enabled: SpectacleCore.annotationDocument.undoStackDepth > 0 height: root.buttonHeight @@ -31,13 +31,10 @@ Grid { text: i18n("Undo") icon.name: "edit-undo" autoRepeat: true - QQC.ToolTip.text: text - QQC.ToolTip.visible: hovered || pressed - QQC.ToolTip.delay: Kirigami.Units.toolTipDelay onClicked: SpectacleCore.annotationDocument.undo() } - QQC.ToolButton { + TtToolButton { enabled: SpectacleCore.annotationDocument.redoStackDepth > 0 height: root.buttonHeight focusPolicy: root.focusPolicy @@ -45,9 +42,6 @@ Grid { text: i18n("Redo") icon.name: "edit-redo" autoRepeat: true - QQC.ToolTip.text: text - QQC.ToolTip.visible: hovered || pressed - QQC.ToolTip.delay: Kirigami.Units.toolTipDelay onClicked: SpectacleCore.annotationDocument.redo() } diff --git a/src/Gui/VideoCaptureOverlay.qml b/src/Gui/VideoCaptureOverlay.qml deleted file mode 100644 index 09ad4d61..00000000 --- a/src/Gui/VideoCaptureOverlay.qml +++ /dev/null @@ -1,243 +0,0 @@ -/* SPDX-FileCopyrightText: 2023 Noah Davis - * SPDX-License-Identifier: LGPL-2.0-or-later - */ - -import QtQuick -import QtQuick.Shapes -import QtQuick.Window -import QtQuick.Layouts -import QtQuick.Controls as QQC -import org.kde.kirigami as Kirigami -import org.kde.spectacle.private - -MouseArea { - id: root - readonly property rect viewportRect: Geometry.mapFromPlatformRect(screenToFollow.geometry, - screenToFollow.devicePixelRatio) - focus: true - acceptedButtons: Qt.LeftButton | Qt.RightButton - hoverEnabled: true - LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft - LayoutMirroring.childrenInherit: true - anchors.fill: parent - enabled: !SpectacleCore.videoPlatform.isRecording - - component Overlay: Rectangle { - color: Settings.useLightMaskColor ? "white" : "black" - opacity: if (SpectacleCore.videoPlatform.isRecording) { - return 0 - } else if (SelectionEditor.selection.empty) { - return 0.25 - } else { - return 0.5 - } - LayoutMirroring.enabled: false - Behavior on opacity { - NumberAnimation { - duration: Kirigami.Units.longDuration - easing.type: Easing.OutCubic - } - } - } - Overlay { // top / full overlay when nothing selected - id: topOverlay - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: selectionRectangle.visible ? selectionRectangle.top : parent.bottom - } - Overlay { // bottom - id: bottomOverlay - anchors.left: parent.left - anchors.top: selectionRectangle.visible ? selectionRectangle.bottom : undefined - anchors.right: parent.right - anchors.bottom: parent.bottom - visible: selectionRectangle.visible && height > 0 - } - Overlay { // left - anchors { - left: topOverlay.left - top: topOverlay.bottom - right: selectionRectangle.visible ? selectionRectangle.left : undefined - bottom: bottomOverlay.top - } - visible: selectionRectangle.visible && height > 0 && width > 0 - } - Overlay { // right - anchors { - left: selectionRectangle.visible ? selectionRectangle.right : undefined - top: topOverlay.bottom - right: topOverlay.right - bottom: bottomOverlay.top - } - visible: selectionRectangle.visible && height > 0 && width > 0 - } - - DashedOutline { - id: selectionRectangle - readonly property real margin: strokeWidth + 1 / Screen.devicePixelRatio - dashSvgPath: SpectacleCore.videoPlatform.isRecording ? svgPath : "" - visible: !SelectionEditor.selection.empty - && Geometry.rectIntersects(Qt.rect(x,y,width,height), Qt.rect(0,0,parent.width, parent.height)) - strokeWidth: dprRound(1) - strokeColor: palette.active.highlight - dashColor: SpectacleCore.videoPlatform.isRecording ? palette.active.base : strokeColor - // We need to be a bit careful about staying out of the recorded area - x: dprFloor(SelectionEditor.selection.x - margin - root.viewportRect.x) - y: dprFloor(SelectionEditor.selection.y - margin - root.viewportRect.y) - width: dprCeil(SelectionEditor.selection.right + margin - root.viewportRect.x) - x - height: dprCeil(SelectionEditor.selection.bottom + margin - root.viewportRect.y) - y - } - - Item { - x: -root.viewportRect.x - y: -root.viewportRect.y - enabled: selectionRectangle.enabled - visible: !SpectacleCore.videoPlatform.isRecording - - component SelectionHandle: Handle { - id: handle - visible: enabled && selectionRectangle.visible - && SelectionEditor.dragLocation === SelectionEditor.None - && Geometry.rectIntersects(Qt.rect(x,y,width,height), root.viewportRect) - fillColor: selectionRectangle.strokeColor - width: Kirigami.Units.gridUnit - height: width - transform: Translate { - x: handle.xOffsetForEdges(selectionRectangle.strokeWidth) - y: handle.yOffsetForEdges(selectionRectangle.strokeWidth) - } - } - - SelectionHandle { - edges: Qt.TopEdge | Qt.LeftEdge - x: dprFloor(SelectionEditor.handlesRect.x) - y: dprFloor(SelectionEditor.handlesRect.y) - } - SelectionHandle { - edges: Qt.LeftEdge - x: dprFloor(SelectionEditor.handlesRect.x) - y: dprRound(SelectionEditor.handlesRect.y + SelectionEditor.handlesRect.height/2 - height/2) - } - SelectionHandle { - edges: Qt.LeftEdge | Qt.BottomEdge - x: dprFloor(SelectionEditor.handlesRect.x) - y: dprCeil(SelectionEditor.handlesRect.y + SelectionEditor.handlesRect.height - height) - } - SelectionHandle { - edges: Qt.TopEdge - x: dprRound(SelectionEditor.handlesRect.x + SelectionEditor.handlesRect.width/2 - width/2) - y: dprFloor(SelectionEditor.handlesRect.y) - } - SelectionHandle { - edges: Qt.BottomEdge - x: dprRound(SelectionEditor.handlesRect.x + SelectionEditor.handlesRect.width/2 - width/2) - y: dprCeil(SelectionEditor.handlesRect.y + SelectionEditor.handlesRect.height - height) - } - SelectionHandle { - edges: Qt.RightEdge - x: dprCeil(SelectionEditor.handlesRect.x + SelectionEditor.handlesRect.width - width) - y: dprRound(SelectionEditor.handlesRect.y + SelectionEditor.handlesRect.height/2 - height/2) - } - SelectionHandle { - edges: Qt.TopEdge | Qt.RightEdge - x: dprCeil(SelectionEditor.handlesRect.x + SelectionEditor.handlesRect.width - width) - y: dprFloor(SelectionEditor.handlesRect.y) - } - SelectionHandle { - edges: Qt.RightEdge | Qt.BottomEdge - x: dprCeil(SelectionEditor.handlesRect.x + SelectionEditor.handlesRect.width - width) - y: dprCeil(SelectionEditor.handlesRect.y + SelectionEditor.handlesRect.height - height) - } - } - - Item { // separate item because it needs to be above the stuff defined above - visible: !SpectacleCore.videoPlatform.isRecording - width: SelectionEditor.screensRect.width - height: SelectionEditor.screensRect.height - x: -root.viewportRect.x - y: -root.viewportRect.y - - // Size ToolTip - SizeLabel { - id: ssToolTip - readonly property int valignment: { - if (SelectionEditor.selection.empty) { - return Qt.AlignVCenter - } - const margin = Kirigami.Units.mediumSpacing * 2 - const w = width + margin - const h = height + margin - if (SelectionEditor.handlesRect.top >= h) { - return Qt.AlignTop - } else if (SelectionEditor.screensRect.height - SelectionEditor.handlesRect.bottom >= h) { - return Qt.AlignBottom - } else { - // At the bottom of the inside of the selection rect. - return Qt.AlignBaseline - } - } - readonly property bool normallyVisible: !SelectionEditor.selection.empty - Binding on x { - value: contextWindow.dprRound(SelectionEditor.selection.horizontalCenter - ssToolTip.width / 2) - when: ssToolTip.normallyVisible - restoreMode: Binding.RestoreNone - } - Binding on y { - value: { - let v = 0 - if (ssToolTip.valignment & Qt.AlignBaseline) { - v = Math.min(SelectionEditor.selection.bottom, SelectionEditor.handlesRect.bottom - Kirigami.Units.gridUnit) - - ssToolTip.height - Kirigami.Units.mediumSpacing * 2 - } else if (ssToolTip.valignment & Qt.AlignTop) { - v = SelectionEditor.handlesRect.top - - ssToolTip.height - Kirigami.Units.mediumSpacing * 2 - } else if (ssToolTip.valignment & Qt.AlignBottom) { - v = SelectionEditor.handlesRect.bottom + Kirigami.Units.mediumSpacing * 2 - } else { - v = (root.height - ssToolTip.height) / 2 - parent.y - } - return contextWindow.dprRound(v) - } - when: ssToolTip.normallyVisible - restoreMode: Binding.RestoreNone - } - visible: opacity > 0 - opacity: ssToolTip.normallyVisible - && Geometry.rectIntersects(Qt.rect(x,y,width,height), root.viewportRect) - Behavior on opacity { - NumberAnimation { - duration: Kirigami.Units.longDuration - easing.type: Easing.OutCubic - } - } - size: Geometry.rawSize(SelectionEditor.selection.size, SelectionEditor.devicePixelRatio) // TODO: real pixel size on wayland - padding: Kirigami.Units.mediumSpacing * 2 - topPadding: padding - QmlUtils.fontMetrics.descent - bottomPadding: topPadding - background: FloatingBackground { - implicitWidth: Math.ceil(parent.contentWidth) + parent.leftPadding + parent.rightPadding - implicitHeight: Math.ceil(parent.contentHeight) + parent.topPadding + parent.bottomPadding - color: Qt.rgba(parent.palette.window.r, - parent.palette.window.g, - parent.palette.window.b, 0.85) - border.color: Qt.rgba(parent.palette.windowText.r, - parent.palette.windowText.g, - parent.palette.windowText.b, 0.2) - border.width: contextWindow.dprRound(1) - } - } - } - - Connections { - target: contextWindow - function onVisibilityChanged(visibility) { - if (visibility !== Window.Hidden && visibility !== Window.Minimized) { - contextWindow.raise() - if (root.containsMouse) { - contextWindow.requestActivate() - } - } - } - } -} diff --git a/src/Platforms/ImagePlatform.h b/src/Platforms/ImagePlatform.h index 7e51db95..de766f52 100644 --- a/src/Platforms/ImagePlatform.h +++ b/src/Platforms/ImagePlatform.h @@ -57,6 +57,7 @@ Q_SIGNALS: void newCroppableScreenshotTaken(const QImage &image); void newScreenshotFailed(const QString &message = {}); + void newScreenshotCanceled(); }; Q_DECLARE_OPERATORS_FOR_FLAGS(ImagePlatform::GrabModes) diff --git a/src/Platforms/ImagePlatformKWin.cpp b/src/Platforms/ImagePlatformKWin.cpp index 237130c9..67fc7375 100644 --- a/src/Platforms/ImagePlatformKWin.cpp +++ b/src/Platforms/ImagePlatformKWin.cpp @@ -468,6 +468,8 @@ void ImagePlatformKWin::trackSource(ScreenShotSource2 *source) Q_EMIT newScreenshotTaken(std::get(result)); } else if (index == ResultVariant::ErrorString) { Q_EMIT newScreenshotFailed(std::get(result)); + } else if (index == ResultVariant::CanceledState) { + Q_EMIT newScreenshotCanceled(); } }); } diff --git a/src/RecordingModeModel.cpp b/src/RecordingModeModel.cpp index 4388daa8..220dcd7e 100644 --- a/src/RecordingModeModel.cpp +++ b/src/RecordingModeModel.cpp @@ -18,6 +18,16 @@ using namespace Qt::StringLiterals; +static std::unique_ptr s_instance; + +RecordingModeModel *RecordingModeModel::instance() +{ + if (!s_instance) { + s_instance = std::make_unique(); + } + return s_instance.get(); +} + RecordingModeModel::RecordingModeModel(QObject *parent) : QAbstractListModel(parent) { @@ -71,6 +81,7 @@ int RecordingModeModel::indexOfRecordingMode(VideoPlatform::RecordingMode mode) void RecordingModeModel::setRecordingModes(VideoPlatform::RecordingModes modes) { + auto count = m_data.size(); m_data.clear(); if (modes & VideoPlatform::Region) { m_data.append({VideoPlatform::Region, recordingModeLabel(VideoPlatform::Region)}); @@ -81,7 +92,10 @@ void RecordingModeModel::setRecordingModes(VideoPlatform::RecordingModes modes) if (modes & VideoPlatform::Window) { m_data.append({VideoPlatform::Window, recordingModeLabel(VideoPlatform::Window)}); } - Q_EMIT countChanged(); + Q_EMIT recordingModesChanged(); + if (count != m_data.size()) { + Q_EMIT countChanged(); + } } QString RecordingModeModel::recordingModeLabel(VideoPlatform::RecordingMode mode) diff --git a/src/RecordingModeModel.h b/src/RecordingModeModel.h index 383f98f4..e56492ae 100644 --- a/src/RecordingModeModel.h +++ b/src/RecordingModeModel.h @@ -7,15 +7,28 @@ #include "Platforms/VideoPlatform.h" #include +#include class RecordingModeModel : public QAbstractListModel { Q_OBJECT QML_ELEMENT + QML_SINGLETON Q_PROPERTY(int count READ rowCount NOTIFY countChanged FINAL) public: explicit RecordingModeModel(QObject *parent = nullptr); + static RecordingModeModel *instance(); + + static RecordingModeModel *create(QQmlEngine *engine, QJSEngine *) + { + auto inst = instance(); + Q_ASSERT(inst); + Q_ASSERT(inst->thread() == engine->thread()); + QJSEngine::setObjectOwnership(inst, QJSEngine::CppOwnership); + return inst; + } + enum { RecordingModeRole = Qt::UserRole + 1, }; @@ -32,6 +45,7 @@ public: Q_SIGNALS: void countChanged(); + void recordingModesChanged(); private: struct Item { diff --git a/src/SpectacleCore.cpp b/src/SpectacleCore.cpp index bfbb3085..820852df 100644 --- a/src/SpectacleCore.cpp +++ b/src/SpectacleCore.cpp @@ -128,6 +128,7 @@ SpectacleCore::SpectacleCore(QObject *parent) connect(SelectionEditor::instance(), &SelectionEditor::accepted, this, [this](const QRectF &rect, const ExportManager::Actions &actions){ ExportManager::instance()->updateTimestamp(); + m_returnToViewer = m_startMode == StartMode::Gui; if (m_videoMode) { const auto captureWindows = CaptureWindow::instances(); SpectacleWindow::setVisibilityForAll(QWindow::Hidden); @@ -160,10 +161,14 @@ SpectacleCore::SpectacleCore(QObject *parent) deleteWindows(); m_annotationDocument->cropCanvas(rect); syncExportImage(); + const auto &exportActions = actions & ExportManager::AnyAction ? actions : autoExportActions(); + const bool willQuit = exportActions.testFlag(ExportManager::AnyAction) // + && exportActions.testFlag(ExportManager::UserAction) // + && Settings::quitAfterSaveCopyExport(); + m_returnToViewer &= !willQuit; showViewerIfGuiMode(); SpectacleWindow::setTitleForAll(SpectacleWindow::Unsaved); ExportManager::instance()->scanQRCode(); - const auto &exportActions = actions & ExportManager::AnyAction ? actions : autoExportActions(); ExportManager::instance()->exportImage(exportActions, outputUrl()); } }); @@ -173,6 +178,7 @@ SpectacleCore::SpectacleCore(QObject *parent) m_annotationDocument->setBaseImage(image); setExportImage(image); ExportManager::instance()->updateTimestamp(); + m_returnToViewer = true; showViewerIfGuiMode(); SpectacleWindow::setTitleForAll(SpectacleWindow::Unsaved); ExportManager::instance()->scanQRCode(); @@ -237,6 +243,21 @@ SpectacleCore::SpectacleCore(QObject *parent) auto uiMessage = i18nc("@info", "An error occurred while taking a screenshot."); onScreenshotOrRecordingFailed(message, uiMessage, &SpectacleCore::dbusScreenshotFailed, &ViewerWindow::showScreenshotFailedMessage); }); + connect(imagePlatform, &ImagePlatform::newScreenshotCanceled, this, [this]() { + if (m_startMode != StartMode::Gui || !m_returnToViewer || isGuiNull()) { + Q_EMIT allDone(); + return; + } + SpectacleWindow::setTitleForAll(SpectacleWindow::Previous); + const auto windows = SpectacleWindow::instances(); + if (windows.empty()) { + initViewerWindow(ViewerWindow::Image); + return; + } + for (auto w : windows) { + w->setVisible(true); + } + }); auto videoPlatform = m_videoPlatform.get(); connect(videoPlatform, &VideoPlatform::recordingChanged, this, [this](bool isRecording) { @@ -314,11 +335,19 @@ SpectacleCore::SpectacleCore(QObject *parent) ExportManager::instance()->exportVideo(autoExportActions() | ExportManager::Save, fileUrl, videoOutputUrl()); }); connect(videoPlatform, &VideoPlatform::recordingCanceled, this, [this] { - if (m_startMode != StartMode::Gui || isGuiNull()) { + if (m_startMode != StartMode::Gui || !m_returnToViewer || isGuiNull()) { Q_EMIT allDone(); return; } SpectacleWindow::setTitleForAll(SpectacleWindow::Previous); + const auto windows = SpectacleWindow::instances(); + if (windows.empty()) { + initViewerWindow(ViewerWindow::Image); + return; + } + for (auto w : windows) { + w->setVisible(true); + } }); connect(videoPlatform, &VideoPlatform::recordingFailed, this, [onScreenshotOrRecordingFailed](const QString &message){ auto uiMessage = i18nc("@info", "An error occurred while attempting to record the screen."); @@ -578,7 +607,7 @@ void SpectacleCore::activate(const QStringList &arguments, const QString &workin } } - if (parser.optionNames().size() > 0 || m_startMode != StartMode::Gui) { + if (parser.optionNames().size() > 0 || m_startMode != StartMode::Gui || !m_returnToViewer) { // Delete windows if we have CLI options or not in GUI mode. // We don't want to delete them otherwise because that will mess with the // settings for PrintScreen key behavior. @@ -672,20 +701,33 @@ void SpectacleCore::activate(const QStringList &arguments, const QString &workin // Determine grab mode using CaptureMode = CaptureModeModel::CaptureMode; using GrabMode = ImagePlatform::GrabMode; - GrabMode grabMode = GrabMode::AllScreens; // Default to all screens - if (m_cliOptions[Option::Fullscreen]) { - grabMode = GrabMode::AllScreens; - } else if (m_cliOptions[Option::Current]) { - grabMode = GrabMode::CurrentScreen; - } else if (m_cliOptions[Option::ActiveWindow]) { - grabMode = GrabMode::ActiveWindow; - } else if (m_cliOptions[Option::Region]) { - grabMode = GrabMode::PerScreenImageNative; - } else if (m_cliOptions[Option::WindowUnderCursor]) { - grabMode = GrabMode::WindowUnderCursor; - } else if (Settings::launchAction() == Settings::UseLastUsedCapturemode) { - grabMode = toGrabMode(CaptureMode(Settings::captureMode()), transientOnly); - } + auto cliGrabMode = [&]() -> std::optional { + if (m_cliOptions[Option::Fullscreen]) { + return GrabMode::AllScreens; + } else if (m_cliOptions[Option::Current]) { + return GrabMode::CurrentScreen; + } else if (m_cliOptions[Option::ActiveWindow]) { + return GrabMode::ActiveWindow; + } else if (m_cliOptions[Option::Region]) { + return GrabMode::PerScreenImageNative; + } else if (m_cliOptions[Option::WindowUnderCursor]) { + return GrabMode::WindowUnderCursor; + } + return std::nullopt; + }; + auto launchActionGrabMode = [&] { + switch (Settings::launchAction()) { + case Settings::TakeRectangularScreenshot: + return GrabMode::PerScreenImageNative; + case Settings::TakeFullscreenScreenshot: + return GrabMode::AllScreens; + case Settings::UseLastUsedCapturemode: + return toGrabMode(CaptureMode(Settings::captureMode()), transientOnly); + default: + return GrabMode::NoGrabModes; + } + }; + auto grabMode = cliGrabMode().value_or(m_startMode == StartMode::Background ? GrabMode::AllScreens : launchActionGrabMode()); using RecordingMode = VideoPlatform::RecordingMode; RecordingMode recordingMode = RecordingMode::NoRecordingModes; @@ -855,7 +897,7 @@ void SpectacleCore::takeNewScreenshot(int captureMode, int timeout, bool include void SpectacleCore::cancelScreenshot() { - if (m_startMode != StartMode::Gui) { + if (m_startMode != StartMode::Gui || !m_returnToViewer) { Q_EMIT allDone(); return; } @@ -883,7 +925,7 @@ void SpectacleCore::showErrorMessage(const QString &message) void SpectacleCore::showViewerIfGuiMode(bool minimized) { - if (m_startMode != StartMode::Gui) { + if (m_startMode != StartMode::Gui || !m_returnToViewer) { return; } initViewerWindow(ViewerWindow::Image); @@ -1140,6 +1182,7 @@ void SpectacleCore::initViewerWindow(ViewerWindow::Mode mode) { // always switch to gui mode when a viewer window is used. m_startMode = SpectacleCore::StartMode::Gui; + m_returnToViewer = true; deleteWindows(); // Transparency isn't needed for this window. @@ -1168,6 +1211,12 @@ void SpectacleCore::startRecording(VideoPlatform::RecordingMode mode, bool withP if (m_videoPlatform->isRecording() || mode == VideoPlatform::NoRecordingModes) { return; } + if (!CaptureWindow::instances().empty()) { + SpectacleWindow::setVisibilityForAll(QWindow::Hidden); + if (mode != VideoPlatform::Region) { + m_returnToViewer = true; + } + } m_lastRecordingMode = mode; setVideoMode(true); const auto &output = m_outputUrl.isLocalFile() ? videoOutputUrl() : QUrl(); @@ -1191,6 +1240,10 @@ void SpectacleCore::setVideoMode(bool videoMode) return; } m_videoMode = videoMode; + if (!videoMode && m_annotationDocument->baseImage().isNull()) { + // Change this if there ends up being a way to toggle video mode outside of rectangle capture mode. + takeNewScreenshot(ImagePlatform::PerScreenImageNative, 0, Settings::includePointer(), Settings::includeDecorations(), Settings::includeShadow()); + } Q_EMIT videoModeChanged(videoMode); } diff --git a/src/SpectacleCore.h b/src/SpectacleCore.h index 246e8944..23d65ead 100644 --- a/src/SpectacleCore.h +++ b/src/SpectacleCore.h @@ -37,7 +37,7 @@ class SpectacleCore : public QObject Q_PROPERTY(int captureTimeRemaining READ captureTimeRemaining NOTIFY captureTimeRemainingChanged FINAL) Q_PROPERTY(qreal captureProgress READ captureProgress NOTIFY captureProgressChanged FINAL) Q_PROPERTY(QString recordedTime READ recordedTime NOTIFY recordedTimeChanged) - Q_PROPERTY(bool videoMode READ videoMode NOTIFY videoModeChanged) + Q_PROPERTY(bool videoMode READ videoMode WRITE setVideoMode NOTIFY videoModeChanged) Q_PROPERTY(QUrl currentVideo READ currentVideo NOTIFY currentVideoChanged) Q_PROPERTY(AnnotationDocument *annotationDocument READ annotationDocument CONSTANT FINAL) @@ -67,15 +67,21 @@ public: int captureTimeRemaining() const; qreal captureProgress() const; + QString recordedTime() const; + + bool videoMode() const; + void setVideoMode(bool enabled); + + QUrl currentVideo() const; + + void initGuiNoScreenshot(); void syncExportImage(); Q_INVOKABLE void startRecording(VideoPlatform::RecordingMode mode, bool withPointer = Settings::videoIncludePointer()); Q_INVOKABLE void finishRecording(); - bool videoMode() const; - QUrl currentVideo() const; - QString recordedTime() const; + Q_INVOKABLE QString timeFromMilliseconds(qint64 milliseconds) const; ExportManager::Actions autoExportActions() const; @@ -140,13 +146,13 @@ private: void initViewerWindow(ViewerWindow::Mode mode); void deleteWindows(); void unityLauncherUpdate(const QVariantMap &properties) const; - void setVideoMode(bool enabled); void setCurrentVideo(const QUrl ¤tVideo); QUrl videoOutputUrl() const; static SpectacleCore *s_self; std::unique_ptr m_annotationDocument = nullptr; StartMode m_startMode = StartMode::Gui; + bool m_returnToViewer = false; QUrl m_screenCaptureUrl; std::unique_ptr m_imagePlatform; std::unique_ptr m_videoPlatform;