nix-stuff/roles/kde/patches/spectacle/pr431.patch
2025-03-05 02:20:39 +01:00

3393 lines
140 KiB
Diff

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<CaptureModeModel> s_instance;
+
+CaptureModeModel *CaptureModeModel::instance()
+{
+ if (!s_instance) {
+ s_instance = std::make_unique<CaptureModeModel>();
+ }
+ 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 <QAbstractListModel>
+#include <QQmlEngine>
/**
* 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 <noahadvs@gmail.com>
+ * 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 <noahadvs@gmail.com>
+ * 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 <noahadvs@gmail.com>
+ * 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 <noahadvs@gmail.com>
+ * 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 <noahadvs@gmail.com>
+ * 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 <KAboutData>
+#include <KLocalizedString>
#include <QApplication>
#include <QDialog>
@@ -13,6 +14,8 @@
#include <cstring>
+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 <noahadvs@gmail.com>
+ * 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 <noahadvs@gmail.com>
- * 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 <noahadvs@gmail.com>
+ * 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 <KLocalizedString>
#include <KStandardActions>
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QList>
#include <QStyle>
+#include <QWidgetAction>
using namespace Qt::StringLiterals;
@@ -21,158 +27,129 @@ static QPointer<OptionsMenu> 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<double>(&SmartSpinBox::valueChanged), this, [this](){
- if (updatingDelayActionLayout) {
+ QObject::connect(spinbox, qOverload<double>(&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<CaptureModeModel>();
- }
-
- // 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 <QActionGroup>
#include <QHBoxLayout>
#include <QLabel>
-#include <QList>
#include <QQmlEngine>
#include <QWidgetAction>
-#include <memory>
-
-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<QAction *> captureModeActions;
- const std::unique_ptr<QAction> captureModeSection;
- const std::unique_ptr<QActionGroup> captureModeGroup;
- const std::unique_ptr<QAction> captureSettingsSection;
- const std::unique_ptr<QAction> includeMousePointerAction;
- const std::unique_ptr<QAction> includeWindowDecorationsAction;
- const std::unique_ptr<QAction> includeWindowShadowAction;
- const std::unique_ptr<QAction> onlyCapturePopupAction;
- const std::unique_ptr<QAction> quitAfterSaveAction;
- const std::unique_ptr<QAction> captureOnClickAction;
- const std::unique_ptr<QWidgetAction> delayAction;
- const std::unique_ptr<QWidget> delayWidget;
- const std::unique_ptr<QHBoxLayout> delayLayout;
- const std::unique_ptr<QLabel> delayLabel;
- const std::unique_ptr<SmartSpinBox> delaySpinBox;
-
- std::unique_ptr<CaptureModeModel> captureModeModel;
-
- bool captureModesInitialized = false;
- bool shouldUpdateCaptureModes = true;
- bool updatingDelayActionLayout = false;
- bool captureModeOptionsEnabled = true;
-
- friend class OptionsMenuSingleton;
+ const std::unique_ptr<QWidgetAction> m_delayAction;
+ const std::unique_ptr<QWidget> m_delayWidget;
+ const std::unique_ptr<QHBoxLayout> m_delayLayout;
+ const std::unique_ptr<QLabel> m_delayLabel;
+ const std::unique_ptr<SmartSpinBox> 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 <noahadvs@gmail.com>
+ * 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 <noahadvs@gmail.com>
+ * 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 <noahadvs@gmail.com>
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#include "RecordingModeMenu.h"
+#include "RecordingModeModel.h"
+#include "SpectacleCore.h"
+#include "ShortcutActions.h"
+#include <KGlobalAccel>
+
+using namespace Qt::StringLiterals;
+
+static QPointer<RecordingModeMenu> 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<VideoPlatform::RecordingMode>();
+ QAction *globalAction = nullptr;
+ auto globalShortcuts = [](QAction *globalAction) {
+ if (!globalAction) {
+ return QList<QKeySequence>{};
+ }
+ 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 <noahadvs@gmail.com>
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#pragma once
+
+#include "SpectacleMenu.h"
+#include <QQmlEngine>
+
+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 <noahadvs@gmail.com>
+ * 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 <noahadvs@gmail.com>
+ * 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 <noahadvs@gmail.com>
+ * 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 <noahadvs@gmail.com>
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#include "ScreenshotModeMenu.h"
+#include "CaptureModeModel.h"
+#include "SpectacleCore.h"
+#include "ShortcutActions.h"
+#include <KGlobalAccel>
+
+using namespace Qt::StringLiterals;
+
+static QPointer<ScreenshotModeMenu> 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<CaptureModeModel::CaptureMode>();
+ QAction *globalAction = nullptr;
+ auto globalShortcuts = [](QAction *globalAction) {
+ if (!globalAction) {
+ return QList<QKeySequence>{};
+ }
+ 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 <noahadvs@gmail.com>
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#pragma once
+
+#include "SpectacleMenu.h"
+#include <QQmlEngine>
+
+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 <noahadvs@gmail.com>
+ * 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 @@
</item>
<item row="0" column="1">
<widget class="QComboBox" name="kcfg_launchAction">
+ <item>
+ <property name="text">
+ <string>Take rectangular screenshot</string>
+ </property>
+ </item>
<item>
<property name="text">
<string>Take full screen screenshot</string>
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 @@
<entry name="launchAction" type="Enum">
<label>What to do when Spectacle is launched</label>
<choices>
+ <choice name="TakeRectangularScreenshot"></choice>
<choice name="TakeFullscreenScreenshot"></choice>
<choice name="UseLastUsedCapturemode"></choice>
<choice name="DoNotTakeScreenshot"></choice>
</choices>
- <default>TakeFullscreenScreenshot</default>
+ <default>TakeRectangularScreenshot</default>
</entry>
<entry name="printKeyRunningAction" type="Enum">
<label>What should happen if print key is pressed when Spectacle is already running</label>
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 <noahadvs@gmail.com>
+ * 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 <noahadvs@gmail.com>
- * 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<ResultVariant::Image>(result));
} else if (index == ResultVariant::ErrorString) {
Q_EMIT newScreenshotFailed(std::get<ResultVariant::ErrorString>(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<RecordingModeModel> s_instance;
+
+RecordingModeModel *RecordingModeModel::instance()
+{
+ if (!s_instance) {
+ s_instance = std::make_unique<RecordingModeModel>();
+ }
+ 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 <QAbstractListModel>
+#include <QQmlEngine>
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<GrabMode> {
+ 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 &currentVideo);
QUrl videoOutputUrl() const;
static SpectacleCore *s_self;
std::unique_ptr<AnnotationDocument> m_annotationDocument = nullptr;
StartMode m_startMode = StartMode::Gui;
+ bool m_returnToViewer = false;
QUrl m_screenCaptureUrl;
std::unique_ptr<ImagePlatform> m_imagePlatform;
std::unique_ptr<VideoPlatform> m_videoPlatform;