3393 lines
140 KiB
Diff
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 ¤tVideo);
|
|
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;
|