├── logo.png ├── doc ├── Annotate.png ├── MainWindow.png ├── SaveOptions.png ├── ApplicationPreferences.png └── CMakeLists.txt ├── .git-blame-ignore-revs ├── .flatpak-manifest.json.license ├── po ├── ca │ └── docs │ │ └── spectacle │ │ ├── Annotate.png │ │ ├── MainWindow.png │ │ ├── SaveOptions.png │ │ └── ApplicationPreferences.png ├── uk │ └── docs │ │ └── spectacle │ │ ├── Annotate.png │ │ ├── MainWindow.png │ │ ├── SaveOptions.png │ │ └── ApplicationPreferences.png └── it │ └── docs │ └── spectacle │ ├── MainWindow.png │ ├── SaveOptions.png │ └── ApplicationPreferences.png ├── icons ├── 256-status-media-recording.kra ├── 256-status-media-recording.webp ├── 256-status-media-recording-pulse.kra ├── 256-status-media-recording-pulse.webp ├── 256-status-media-recording-started.kra ├── 256-status-media-recording-started.webp ├── set-pulse-loops.sh ├── set-started-loops.sh ├── CMakeLists.txt └── sc-apps-spectacle.svg ├── CMakePresets.json.license ├── dbus ├── app-org.kde.spectacle.service.in └── CMakeLists.txt ├── src ├── Gui │ ├── SettingsDialog │ │ ├── settings.kcfgc │ │ ├── ImageSaveOptionsPage.h │ │ ├── VideoFormatComboBox.h │ │ ├── VideoSaveOptionsPage.h │ │ ├── ShortcutsOptionsPage.h │ │ ├── SettingsUtils.h │ │ ├── VideoFormatComboBox.cpp │ │ ├── SettingsDialog.h │ │ ├── GeneralOptionsPage.h │ │ ├── ShortcutsOptionsPage.cpp │ │ ├── GeneralOptionsPage.cpp │ │ ├── VideoSaveOptions.ui │ │ ├── VideoSaveOptionsPage.cpp │ │ ├── OcrLanguageSelector.h │ │ ├── ImageSaveOptionsPage.cpp │ │ └── SettingsDialog.cpp │ ├── SaveAsAction.qml │ ├── AcceptAction.qml │ ├── CancelAction.qml │ ├── CopyLocationAction.qml │ ├── TextContextMenuConnection.qml │ ├── FloatingBackground.qml │ ├── RecordAction.qml │ ├── HelpMenuButton.qml │ ├── OptionsMenuButton.qml │ ├── EditAction.qml │ ├── OcrAction.qml │ ├── RecordingModeMenuButton.qml │ ├── SaveAction.qml │ ├── CopyImageAction.qml │ ├── ScreenshotModeMenuButton.qml │ ├── SmartSpinBox.h │ ├── ExportMenuButton.qml │ ├── TtToolButton.qml │ ├── RecordingModeMenu.h │ ├── SceenshotModeMenu.qml │ ├── ScreenshotModeMenu.h │ ├── RecordingSettingsColumn.qml │ ├── HelpMenu.h │ ├── WidgetWindowUtils.h │ ├── RecordingModeButtonsColumn.qml │ ├── SpectacleMenu.h │ ├── AnimatedLoader.qml │ ├── SizeLabel.qml │ ├── EmptyPage.qml │ ├── SmartSpinBox.cpp │ ├── TextContextMenu.h │ ├── NewScreenshotToolButton.qml │ ├── AnnotationEditor.qml │ ├── RecordOptions.qml │ ├── OptionsMenu.h │ ├── CaptureWindow.h │ ├── FloatingToolBar.qml │ ├── CaptureOptions.qml │ ├── ExportMenu.h │ ├── ViewerWindow.h │ ├── ButtonGrid.qml │ ├── UndoRedoGroup.qml │ ├── DashedOutline.qml │ ├── CaptureModeButtonsColumn.qml │ ├── Magnifier.qml │ ├── SpectacleMenu.cpp │ ├── InlineMessageModel.h │ ├── Outline.qml │ ├── RecordingModeMenu.cpp │ ├── HelpMenu.cpp │ ├── ScreenshotModeMenu.cpp │ ├── QmlUtils.qml │ ├── CaptureSettingsColumn.qml │ ├── Selection.h │ ├── SelectionEditor.h │ ├── SpectacleWindow.h │ └── InlineMessageModel.cpp ├── Platforms │ ├── ImagePlatform.cpp │ ├── PlatformLoader.h │ ├── VideoPlatformWayland.h │ ├── PlatformNull.h │ ├── screencasting.h │ ├── PlatformNull.cpp │ ├── ImagePlatform.h │ ├── ImagePlatformXcb.h │ ├── VideoPlatform.cpp │ └── PlatformLoader.cpp ├── CommandLineOptions.cpp ├── Config.h.in ├── ScreenShotEffect.h ├── PlasmaVersion.h ├── ShortcutActions.h ├── DebugUtils.h ├── VideoFormatModel.h ├── SpectacleDBusAdapter.h ├── ScreenShotEffect.cpp ├── RecordingModeModel.h ├── CaptureModeModel.h ├── PlasmaVersion.cpp ├── VideoFormatModel.cpp ├── SpectacleDBusAdapter.cpp ├── QtCV.h ├── RecordingModeModel.cpp └── ImageMetaData.h ├── sanitizers.supp ├── .gitignore ├── .gitlab-ci.yml ├── Messages.sh ├── LICENSES ├── LicenseRef-KDE-Accepted-GPL.txt └── BSD-3-Clause.txt ├── desktop └── CMakeLists.txt ├── tests └── CMakeLists.txt ├── .flatpak-manifest.json ├── .kde-ci.yml ├── cmake └── tesseract_test.cpp ├── kconf_update ├── spectacle-24.02.0-keep_old_filename_templates.cpp ├── spectacle-24.02.0-keep_old_save_location.cpp ├── CMakeLists.txt ├── spectacle-24.02.0-video_format.cpp ├── spectacle-24.02.0-rename_settings.cpp ├── spectacle.upd ├── spectacle-24.02.0-change_placeholder_format.cpp └── ConfigUtils.h ├── README.md ├── CONTRIBUTING.md └── snapcraft.yaml /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KDE/spectacle/HEAD/logo.png -------------------------------------------------------------------------------- /doc/Annotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KDE/spectacle/HEAD/doc/Annotate.png -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # clang-format 2 | 217a5391029e3a45615fadec1f35cae753a16651 3 | -------------------------------------------------------------------------------- /doc/MainWindow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KDE/spectacle/HEAD/doc/MainWindow.png -------------------------------------------------------------------------------- /doc/SaveOptions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KDE/spectacle/HEAD/doc/SaveOptions.png -------------------------------------------------------------------------------- /.flatpak-manifest.json.license: -------------------------------------------------------------------------------- 1 | SPDX-License-Identifier: CC0-1.0 2 | SPDX-FileCopyrightText: none 3 | -------------------------------------------------------------------------------- /doc/ApplicationPreferences.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KDE/spectacle/HEAD/doc/ApplicationPreferences.png -------------------------------------------------------------------------------- /po/ca/docs/spectacle/Annotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KDE/spectacle/HEAD/po/ca/docs/spectacle/Annotate.png -------------------------------------------------------------------------------- /po/uk/docs/spectacle/Annotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KDE/spectacle/HEAD/po/uk/docs/spectacle/Annotate.png -------------------------------------------------------------------------------- /icons/256-status-media-recording.kra: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KDE/spectacle/HEAD/icons/256-status-media-recording.kra -------------------------------------------------------------------------------- /icons/256-status-media-recording.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KDE/spectacle/HEAD/icons/256-status-media-recording.webp -------------------------------------------------------------------------------- /po/ca/docs/spectacle/MainWindow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KDE/spectacle/HEAD/po/ca/docs/spectacle/MainWindow.png -------------------------------------------------------------------------------- /po/ca/docs/spectacle/SaveOptions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KDE/spectacle/HEAD/po/ca/docs/spectacle/SaveOptions.png -------------------------------------------------------------------------------- /po/it/docs/spectacle/MainWindow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KDE/spectacle/HEAD/po/it/docs/spectacle/MainWindow.png -------------------------------------------------------------------------------- /po/it/docs/spectacle/SaveOptions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KDE/spectacle/HEAD/po/it/docs/spectacle/SaveOptions.png -------------------------------------------------------------------------------- /po/uk/docs/spectacle/MainWindow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KDE/spectacle/HEAD/po/uk/docs/spectacle/MainWindow.png -------------------------------------------------------------------------------- /po/uk/docs/spectacle/SaveOptions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KDE/spectacle/HEAD/po/uk/docs/spectacle/SaveOptions.png -------------------------------------------------------------------------------- /icons/256-status-media-recording-pulse.kra: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KDE/spectacle/HEAD/icons/256-status-media-recording-pulse.kra -------------------------------------------------------------------------------- /CMakePresets.json.license: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Laurent Montel 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | -------------------------------------------------------------------------------- /icons/256-status-media-recording-pulse.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KDE/spectacle/HEAD/icons/256-status-media-recording-pulse.webp -------------------------------------------------------------------------------- /icons/256-status-media-recording-started.kra: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KDE/spectacle/HEAD/icons/256-status-media-recording-started.kra -------------------------------------------------------------------------------- /icons/256-status-media-recording-started.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KDE/spectacle/HEAD/icons/256-status-media-recording-started.webp -------------------------------------------------------------------------------- /icons/set-pulse-loops.sh: -------------------------------------------------------------------------------- 1 | #!bash 2 | webpmux -set loop 1 ./256-status-media-recording-pulse.webp -o ./256-status-media-recording-pulse.webp 3 | -------------------------------------------------------------------------------- /po/ca/docs/spectacle/ApplicationPreferences.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KDE/spectacle/HEAD/po/ca/docs/spectacle/ApplicationPreferences.png -------------------------------------------------------------------------------- /po/it/docs/spectacle/ApplicationPreferences.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KDE/spectacle/HEAD/po/it/docs/spectacle/ApplicationPreferences.png -------------------------------------------------------------------------------- /po/uk/docs/spectacle/ApplicationPreferences.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KDE/spectacle/HEAD/po/uk/docs/spectacle/ApplicationPreferences.png -------------------------------------------------------------------------------- /icons/set-started-loops.sh: -------------------------------------------------------------------------------- 1 | #!bash 2 | webpmux -set loop 3 ./256-status-media-recording-started.webp -o ./256-status-media-recording-started.webp 3 | -------------------------------------------------------------------------------- /dbus/app-org.kde.spectacle.service.in: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Spectacle screenshot capture utility 3 | 4 | [Service] 5 | ExecStart=@KDE_INSTALL_FULL_BINDIR@/spectacle --dbus 6 | BusName=org.kde.Spectacle 7 | KillMode=process 8 | -------------------------------------------------------------------------------- /src/Gui/SettingsDialog/settings.kcfgc: -------------------------------------------------------------------------------- 1 | File=spectacle.kcfg 2 | ClassName=Settings 3 | GlobalEnums=true 4 | Mutators=true 5 | GenerateProperties=true 6 | Singleton=true 7 | DefaultValueGetters=true 8 | QmlRegistration=true 9 | -------------------------------------------------------------------------------- /doc/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # documentation 2 | 3 | KDOCTOOLS_CREATE_HANDBOOK( 4 | index.docbook 5 | INSTALL_DESTINATION ${KDE_INSTALL_DOCBUNDLEDIR}/en 6 | SUBDIR spectacle 7 | ) 8 | 9 | KDOCTOOLS_CREATE_MANPAGE( 10 | man-spectacle.1.docbook 1 11 | INSTALL_DESTINATION 12 | ${KDE_INSTALL_MANDIR} 13 | ) 14 | -------------------------------------------------------------------------------- /src/Gui/SaveAsAction.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2025 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick.Templates as T 6 | 7 | T.Action { 8 | icon.name: "document-save-as" 9 | text: i18nc("@action", "Save As…") 10 | onTriggered: contextWindow.saveAs() 11 | } 12 | -------------------------------------------------------------------------------- /src/Gui/AcceptAction.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2025 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick.Templates as T 6 | 7 | T.Action { 8 | icon.name: "dialog-ok" 9 | text: i18nc("@action accept selection", "Accept") 10 | onTriggered: contextWindow.accept() 11 | } 12 | -------------------------------------------------------------------------------- /src/Gui/CancelAction.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2025 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick.Templates as T 6 | 7 | T.Action { 8 | icon.name: "dialog-cancel" 9 | text: i18nc("@action cancel selection", "Cancel") 10 | onTriggered: contextWindow.cancel() 11 | } 12 | -------------------------------------------------------------------------------- /src/Gui/CopyLocationAction.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2025 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick.Templates as T 6 | 7 | T.Action { 8 | icon.name: "edit-copy-path" 9 | text: i18nc("@action", "Copy Location") 10 | onTriggered: contextWindow.copyLocation() 11 | } 12 | -------------------------------------------------------------------------------- /src/Gui/TextContextMenuConnection.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2023 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick 2.15 6 | 7 | Connections { 8 | function onPressed(event) { 9 | if (event.button === Qt.RightButton) { 10 | TextContextMenu.popup(target) 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Platforms/ImagePlatform.cpp: -------------------------------------------------------------------------------- 1 | /* This file is part of Spectacle, the KDE screenshot utility 2 | * SPDX-FileCopyrightText: 2019 Boudhayan Gupta 3 | * SPDX-License-Identifier: LGPL-2.0-or-later 4 | */ 5 | 6 | #include "ImagePlatform.h" 7 | 8 | ImagePlatform::ImagePlatform(QObject *parent) 9 | : QObject(parent) 10 | { 11 | } 12 | 13 | #include "moc_ImagePlatform.cpp" 14 | -------------------------------------------------------------------------------- /src/Gui/FloatingBackground.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick 6 | import org.kde.kirigami as Kirigami 7 | 8 | Kirigami.ShadowedRectangle { 9 | radius: Kirigami.Units.mediumSpacing / 2 10 | shadow.color: Qt.rgba(0,0,0,0.2) 11 | shadow.size: 9 12 | shadow.yOffset: 2 13 | } 14 | -------------------------------------------------------------------------------- /sanitizers.supp: -------------------------------------------------------------------------------- 1 | # Suppression file for ASAN/LSAN 2 | 3 | leak:libspeechd 4 | leak:getdelim 5 | leak:g_malloc 6 | leak:libfontconfig 7 | leak:libdbus 8 | leak:QEasingCurve:: 9 | leak:QtSharedPointer::ExternalRefCountData::getAndRef 10 | leak:QArrayData::allocate 11 | leak:QObject::QObject 12 | leak:QObjectPrivate::addConnection 13 | leak:QObjectPrivate::connectImpl 14 | leak:QPropertyAnimation::QPropertyAnimation 15 | -------------------------------------------------------------------------------- /src/Gui/RecordAction.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2025 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick.Templates as T 6 | import org.kde.spectacle.private 7 | 8 | T.Action { 9 | enabled: SpectacleCore.videoMode 10 | icon.name: "media-record" 11 | text: i18nc("@action start recording", "Record") 12 | onTriggered: contextWindow.accept() 13 | } 14 | -------------------------------------------------------------------------------- /src/Gui/HelpMenuButton.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2025 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick 6 | import org.kde.spectacle.private 7 | 8 | TtToolButton { 9 | icon.name: "help-contents" 10 | text: i18nc("@action", "Help") 11 | down: pressed || HelpMenu.visible 12 | Accessible.role: Accessible.ButtonMenu 13 | onPressed: HelpMenu.popup(this) 14 | } 15 | -------------------------------------------------------------------------------- /src/Gui/OptionsMenuButton.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2025 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick 6 | import org.kde.spectacle.private 7 | 8 | TtToolButton { 9 | icon.name: "configure" 10 | text: i18nc("@action", "Options") 11 | down: pressed || OptionsMenu.visible 12 | Accessible.role: Accessible.ButtonMenu 13 | onPressed: OptionsMenu.popup(this) 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore the following files 2 | *~ 3 | *.[oa] 4 | *.diff 5 | *.kate-swp 6 | *.kdev4 7 | .kdev_include_paths 8 | *.kdevelop.pcs 9 | *.moc 10 | *.moc.cpp 11 | *.orig 12 | *.user 13 | .*.swp 14 | .swp.* 15 | Doxyfile 16 | Makefile 17 | avail 18 | random_seed 19 | /build*/ 20 | CMakeLists.txt.user* 21 | *.unc-backup* 22 | .cmake/ 23 | .clang-format 24 | /compile_commands.json 25 | .clangd 26 | .idea 27 | /cmake-build* 28 | .cache 29 | .flatpak-builder 30 | -------------------------------------------------------------------------------- /icons/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # default icons for the hicolor theme. 2 | # have ecm install them 3 | 4 | ecm_install_icons( 5 | ICONS 6 | sc-apps-spectacle.svg 7 | DESTINATION ${KDE_INSTALL_ICONDIR} 8 | THEME hicolor 9 | ) 10 | 11 | qt_add_resources(spectacle "spectacle_icons" 12 | PREFIX "/icons" 13 | FILES 14 | 256-status-media-recording-started.webp 15 | 256-status-media-recording-pulse.webp 16 | 256-status-media-recording.webp 17 | ) 18 | -------------------------------------------------------------------------------- /src/CommandLineOptions.cpp: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2023 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #include "CommandLineOptions.h" 6 | 7 | struct CommandLineOptionsSingleton { 8 | CommandLineOptions self; 9 | }; 10 | 11 | Q_GLOBAL_STATIC(CommandLineOptionsSingleton, privateCommandLineOptionsSelf) 12 | 13 | CommandLineOptions *CommandLineOptions::self() 14 | { 15 | return &privateCommandLineOptionsSelf()->self; 16 | } 17 | -------------------------------------------------------------------------------- /src/Gui/EditAction.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2025 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick.Templates as T 6 | import org.kde.spectacle.private 7 | 8 | T.Action { 9 | enabled: !SpectacleCore.videoMode 10 | icon.name: "edit-image" 11 | text: i18nc("@action edit screenshot", "Edit…") 12 | checkable: true 13 | checked: contextWindow.annotating 14 | onToggled: contextWindow.annotating = checked 15 | } 16 | -------------------------------------------------------------------------------- /src/Gui/OcrAction.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2025 Jhair Paris 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick.Templates as T 6 | import org.kde.spectacle.private 7 | 8 | T.Action { 9 | enabled: !SpectacleCore.videoMode && 10 | SpectacleCore.ocrAvailable && 11 | SpectacleCore.ocrStatus !== 1 12 | icon.name: "document-scan" 13 | text: i18nc("@action", "Extract Text") 14 | onTriggered: SpectacleCore.startOcrExtraction() 15 | } 16 | -------------------------------------------------------------------------------- /src/Gui/RecordingModeMenuButton.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2025 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick 6 | import QtQuick.Controls as QQC 7 | import org.kde.spectacle.private 8 | 9 | TtToolButton { 10 | icon.name: "camera-video" 11 | text: i18nc("@action select new recording mode", "New Recording") 12 | down: pressed || RecordingModeMenu.visible 13 | Accessible.role: Accessible.ButtonMenu 14 | onPressed: RecordingModeMenu.popup(this) 15 | } 16 | -------------------------------------------------------------------------------- /src/Gui/SaveAction.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2025 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick.Templates as T 6 | import org.kde.spectacle.private 7 | 8 | T.Action { 9 | // We don't use this in video mode because the video is already 10 | // automatically saved and you can't edit the video. 11 | enabled: !SpectacleCore.videoMode 12 | icon.name: "document-save" 13 | text: i18nc("@action", "Save") 14 | onTriggered: contextWindow.save() 15 | } 16 | -------------------------------------------------------------------------------- /src/Gui/CopyImageAction.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2025 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick.Templates as T 6 | import org.kde.spectacle.private 7 | 8 | T.Action { 9 | // We don't use this in video mode because you can't copy raw video to the 10 | // clipboard, or at least not elegantly. 11 | enabled: !SpectacleCore.videoMode 12 | icon.name: "edit-copy" 13 | text: i18nc("@action", "Copy") 14 | onTriggered: contextWindow.copyImage() 15 | } 16 | -------------------------------------------------------------------------------- /src/Gui/ScreenshotModeMenuButton.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2025 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick 6 | import QtQuick.Controls as QQC 7 | import org.kde.spectacle.private 8 | 9 | TtToolButton { 10 | icon.name: "camera-photo" 11 | text: i18nc("@action select new screenshot mode", "New Screenshot") 12 | down: pressed || ScreenshotModeMenu.visible 13 | Accessible.role: Accessible.ButtonMenu 14 | onPressed: ScreenshotModeMenu.popup(this) 15 | } 16 | -------------------------------------------------------------------------------- /src/Platforms/PlatformLoader.h: -------------------------------------------------------------------------------- 1 | /* This file is part of Spectacle, the KDE screenshot utility 2 | * SPDX-FileCopyrightText: 2019 Boudhayan Gupta 3 | 4 | * SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | #pragma once 8 | 9 | #include "ImagePlatform.h" 10 | #include "VideoPlatform.h" 11 | #include 12 | 13 | using ImagePlatformPtr = std::unique_ptr; 14 | ImagePlatformPtr loadImagePlatform(); 15 | 16 | using VideoPlatformPtr = std::unique_ptr; 17 | VideoPlatformPtr loadVideoPlatform(); 18 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: None 2 | # SPDX-License-Identifier: CC0-1.0 3 | 4 | include: 5 | - project: sysadmin/ci-utilities 6 | file: 7 | - /gitlab-templates/linux-qt6.yml 8 | - /gitlab-templates/freebsd-qt6.yml 9 | # Disable until flatpak CI supports Qt 6. 10 | # - /gitlab-templates/flatpak.yml 11 | - /gitlab-templates/xml-lint.yml 12 | - /gitlab-templates/yaml-lint.yml 13 | - /gitlab-templates/qml-lint.yml 14 | - /gitlab-templates/linux-qt6-next.yml 15 | - /gitlab-templates/documentation.yml 16 | -------------------------------------------------------------------------------- /src/Gui/SmartSpinBox.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2015 Boudhayan Gupta 3 | * 4 | * SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | #ifndef SMARTSPINBOX_H 8 | #define SMARTSPINBOX_H 9 | 10 | #include 11 | 12 | class SmartSpinBox : public QDoubleSpinBox 13 | { 14 | Q_OBJECT 15 | 16 | public: 17 | explicit SmartSpinBox(QWidget *parent = nullptr); 18 | QString textFromValue(double val) const override; 19 | 20 | private Q_SLOTS: 21 | 22 | void suffixChangeHandler(double val); 23 | }; 24 | 25 | #endif // SMARTSPINBOX_H 26 | -------------------------------------------------------------------------------- /src/Gui/ExportMenuButton.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2025 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick 6 | import org.kde.kirigami as Kirigami 7 | import org.kde.spectacle.private 8 | 9 | TtToolButton { 10 | // FIXME: make export menu actually work with videos 11 | visible: !SpectacleCore.videoMode 12 | icon.name: "document-share" 13 | text: i18nc("@action", "Export") 14 | down: pressed || ExportMenu.visible 15 | Accessible.role: Accessible.ButtonMenu 16 | onPressed: ExportMenu.popup(this) 17 | } 18 | -------------------------------------------------------------------------------- /src/Gui/TtToolButton.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2025 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick 6 | import QtQuick.Controls as QQC 7 | import org.kde.kirigami as Kirigami 8 | import org.kde.spectacle.private 9 | 10 | QQC.ToolButton { 11 | implicitHeight: QmlUtils.iconTextButtonHeight 12 | width: display === QQC.ToolButton.IconOnly ? height : implicitWidth 13 | QQC.ToolTip.text: text 14 | QQC.ToolTip.visible: (hovered || pressed) && display === QQC.ToolButton.IconOnly 15 | QQC.ToolTip.delay: Kirigami.Units.toolTipDelay 16 | } 17 | -------------------------------------------------------------------------------- /Messages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Invoke the extractrc script on all .ui, .rc, and .kcfg files in the sources. 4 | # The results are stored in a pseudo .cpp file to be picked up by xgettext. 5 | lst=`find . -name \*.rc -o -name \*.ui -o -name \*.kcfg` 6 | if [ -n "$lst" ] ; then 7 | $EXTRACTRC $lst >> rc.cpp 8 | fi 9 | 10 | # If your framework contains tips-of-the-day, call preparetips as well. 11 | if [ -f "data/tips" ] ; then 12 | ( cd data && $PREPARETIPS > ../tips.cpp ) 13 | fi 14 | 15 | # Run xgettext to extract strings from all source files. 16 | $XGETTEXT `find . -name \*.cpp -o -name \*.h -o -name \*.qml` -o $podir/spectacle.pot 17 | -------------------------------------------------------------------------------- /src/Config.h.in: -------------------------------------------------------------------------------- 1 | #ifndef SPECTACLE_CONFIG_H 2 | #define SPECTACLE_CONFIG_H 3 | 4 | /* Define to 1 if we are building with XCB */ 5 | #cmakedefine XCB_FOUND 1 6 | 7 | /* Define to 1 if we have Purpose */ 8 | #cmakedefine PURPOSE_FOUND 1 9 | 10 | /* Define to 1 if we have Tesseract OCR */ 11 | #cmakedefine HAVE_TESSERACT_OCR 1 12 | 13 | /* Set the Spectacle version from CMake */ 14 | #cmakedefine SPECTACLE_VERSION "@SPECTACLE_VERSION@" 15 | 16 | /* Set the QML module URI from CMake */ 17 | #cmakedefine SPECTACLE_QML_URI "@SPECTACLE_QML_URI@" 18 | 19 | /* Set the QML module URI from CMake */ 20 | static const auto SPECTACLE_QML_PATH = u"@SPECTACLE_QML_PATH@"; 21 | 22 | #endif 23 | -------------------------------------------------------------------------------- /src/Gui/SettingsDialog/ImageSaveOptionsPage.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2015 Boudhayan Gupta 3 | * 4 | * SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | #ifndef SAVEOPTIONSPAGE_H 8 | #define SAVEOPTIONSPAGE_H 9 | 10 | #include 11 | #include 12 | 13 | class Ui_ImageSaveOptions; 14 | 15 | class ImageSaveOptionsPage : public QWidget 16 | { 17 | Q_OBJECT 18 | 19 | public: 20 | explicit ImageSaveOptionsPage(QWidget *parent = nullptr); 21 | ~ImageSaveOptionsPage() override; 22 | 23 | private: 24 | QScopedPointer m_ui; 25 | 26 | void updateFilenamePreview(); 27 | }; 28 | 29 | #endif // SAVEOPTIONSPAGE_H 30 | -------------------------------------------------------------------------------- /src/Gui/SettingsDialog/VideoFormatComboBox.h: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2024 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #pragma once 6 | 7 | #include "VideoFormatModel.h" 8 | #include 9 | 10 | class VideoFormatComboBox : public QComboBox 11 | { 12 | Q_OBJECT 13 | Q_PROPERTY(VideoPlatform::Format currentFormat READ currentFormat WRITE setCurrentFormat NOTIFY currentFormatChanged) 14 | public: 15 | VideoFormatComboBox(VideoFormatModel *model, QWidget *parent = nullptr); 16 | VideoPlatform::Format currentFormat() const; 17 | void setCurrentFormat(VideoPlatform::Format format); 18 | Q_SIGNAL void currentFormatChanged(); 19 | }; 20 | -------------------------------------------------------------------------------- /src/ScreenShotEffect.h: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2023 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #pragma once 6 | 7 | #include 8 | 9 | class ScreenShotEffect 10 | { 11 | public: 12 | static bool isLoaded(); 13 | static quint32 version(); 14 | 15 | enum { 16 | NullVersion = 0 17 | }; 18 | 19 | private: 20 | ScreenShotEffect() = delete; 21 | ~ScreenShotEffect() = delete; 22 | ScreenShotEffect(const ScreenShotEffect &) = delete; 23 | ScreenShotEffect(ScreenShotEffect &&) = delete; 24 | ScreenShotEffect &operator=(const ScreenShotEffect &) = delete; 25 | ScreenShotEffect &operator=(ScreenShotEffect &&) = delete; 26 | }; 27 | -------------------------------------------------------------------------------- /LICENSES/LicenseRef-KDE-Accepted-GPL.txt: -------------------------------------------------------------------------------- 1 | This library is free software; you can redistribute it and/or 2 | modify it under the terms of the GNU General Public License as 3 | published by the Free Software Foundation; either version 3 of 4 | the license or (at your option) at any later version that is 5 | accepted by the membership of KDE e.V. (or its successor 6 | approved by the membership of KDE e.V.), which shall act as a 7 | proxy as defined in Section 14 of version 3 of the license. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | -------------------------------------------------------------------------------- /src/Gui/SettingsDialog/VideoSaveOptionsPage.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2015 Boudhayan Gupta 3 | * 4 | * SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | #pragma once 8 | 9 | #include 10 | #include 11 | 12 | class Ui_VideoSaveOptions; 13 | class VideoFormatComboBox; 14 | class VideoFormatModel; 15 | 16 | class VideoSaveOptionsPage : public QWidget 17 | { 18 | Q_OBJECT 19 | 20 | public: 21 | explicit VideoSaveOptionsPage(QWidget *parent = nullptr); 22 | ~VideoSaveOptionsPage() override; 23 | 24 | private: 25 | QScopedPointer m_ui; 26 | std::unique_ptr m_videoFormatComboBox; 27 | std::unique_ptr m_videoFormatModel; 28 | 29 | void updateFilenamePreview(); 30 | }; 31 | -------------------------------------------------------------------------------- /src/Gui/RecordingModeMenu.h: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2025 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #pragma once 6 | 7 | #include "SpectacleMenu.h" 8 | #include 9 | 10 | class RecordingModeMenu : public SpectacleMenu 11 | { 12 | Q_OBJECT 13 | QML_ELEMENT 14 | QML_SINGLETON 15 | 16 | public: 17 | static RecordingModeMenu *instance(); 18 | 19 | static RecordingModeMenu *create(QQmlEngine *engine, QJSEngine *) 20 | { 21 | auto inst = instance(); 22 | Q_ASSERT(inst); 23 | Q_ASSERT(inst->thread() == engine->thread()); 24 | QJSEngine::setObjectOwnership(inst, QJSEngine::CppOwnership); 25 | return inst; 26 | } 27 | 28 | private: 29 | explicit RecordingModeMenu(QWidget *parent = nullptr); 30 | }; 31 | -------------------------------------------------------------------------------- /src/Gui/SceenshotModeMenu.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2023 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick 6 | import QtQuick.Controls 7 | import org.kde.spectacle.private 8 | 9 | Menu { 10 | id: root 11 | Instantiator { 12 | id: instantiator 13 | model: CaptureModeModel 14 | delegate: Action { 15 | required property var model 16 | text: model.display 17 | onTriggered: (source) => { 18 | Settings.captureMode = model.captureMode 19 | SpectacleCore.takeNewScreenshot() 20 | } 21 | } 22 | 23 | onObjectAdded: (index, object) => root.insertAction(index, object) 24 | onObjectRemoved: (index, object) => root.removeAction(object) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Gui/ScreenshotModeMenu.h: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2025 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #pragma once 6 | 7 | #include "SpectacleMenu.h" 8 | #include 9 | 10 | class ScreenshotModeMenu : public SpectacleMenu 11 | { 12 | Q_OBJECT 13 | QML_ELEMENT 14 | QML_SINGLETON 15 | 16 | public: 17 | static ScreenshotModeMenu *instance(); 18 | 19 | static ScreenshotModeMenu *create(QQmlEngine *engine, QJSEngine *) 20 | { 21 | auto inst = instance(); 22 | Q_ASSERT(inst); 23 | Q_ASSERT(inst->thread() == engine->thread()); 24 | QJSEngine::setObjectOwnership(inst, QJSEngine::CppOwnership); 25 | return inst; 26 | } 27 | 28 | private: 29 | explicit ScreenshotModeMenu(QWidget *parent = nullptr); 30 | }; 31 | -------------------------------------------------------------------------------- /src/Gui/SettingsDialog/ShortcutsOptionsPage.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2019 David Redondo 3 | * 4 | * SPDX-License-Identifier: GPL-2.0-or-later 5 | */ 6 | 7 | #ifndef SHORTCUTSOPTIONSPAGE_H 8 | #define SHORTCUTSOPTIONSPAGE_H 9 | 10 | #include 11 | 12 | class KShortcutsEditor; 13 | 14 | class ShortcutsOptionsPage : public QWidget 15 | { 16 | Q_OBJECT 17 | 18 | public: 19 | explicit ShortcutsOptionsPage(QWidget *parent); 20 | ~ShortcutsOptionsPage() override; 21 | 22 | bool isModified() const; 23 | void defaults() const; 24 | 25 | Q_SIGNALS: 26 | void shortCutsChanged(); 27 | 28 | public Q_SLOTS: 29 | 30 | void saveChanges(); 31 | void resetChanges(); 32 | 33 | private: 34 | KShortcutsEditor *mEditor = nullptr; 35 | }; 36 | 37 | #endif // SHORTCUTSOPTIONSPAGE_H 38 | -------------------------------------------------------------------------------- /src/PlasmaVersion.h: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2023 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #pragma once 6 | 7 | #include 8 | 9 | class PlasmaVersion 10 | { 11 | public: 12 | /** 13 | * Get the plasma version as an unsigned int. 14 | */ 15 | static quint32 get(); 16 | 17 | /** 18 | * Use this for plasma versions the same way you'd use QT_VERSION_CHECK() 19 | */ 20 | static quint32 check(quint8 major, quint8 minor, quint8 patch); 21 | 22 | private: 23 | PlasmaVersion() = delete; 24 | ~PlasmaVersion() = delete; 25 | PlasmaVersion(const PlasmaVersion &) = delete; 26 | PlasmaVersion(PlasmaVersion &&) = delete; 27 | PlasmaVersion &operator=(const PlasmaVersion &) = delete; 28 | PlasmaVersion &operator=(PlasmaVersion &&) = delete; 29 | }; 30 | -------------------------------------------------------------------------------- /desktop/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # install the .desktop and rc files in the correct place 2 | 3 | configure_file(org.kde.spectacle.desktop.cmake ${CMAKE_CURRENT_BINARY_DIR}/org.kde.spectacle.desktop) 4 | install( 5 | PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/org.kde.spectacle.desktop 6 | DESTINATION ${KDE_INSTALL_APPDIR} 7 | ) 8 | 9 | install( DIRECTORY DESTINATION "${KDE_INSTALL_FULL_DATAROOTDIR}/kglobalaccel" ) 10 | install( 11 | CODE "execute_process(COMMAND \"${CMAKE_COMMAND}\" -E create_symlink \"${KDE_INSTALL_FULL_APPDIR}/org.kde.spectacle.desktop\" \"\$ENV{DESTDIR}${KDE_INSTALL_FULL_DATAROOTDIR}/kglobalaccel/org.kde.spectacle.desktop\")" 12 | ) 13 | 14 | install( 15 | FILES spectacle.notifyrc 16 | DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR} 17 | ) 18 | 19 | install( 20 | FILES org.kde.spectacle.appdata.xml 21 | DESTINATION ${KDE_INSTALL_METAINFODIR} 22 | ) 23 | -------------------------------------------------------------------------------- /src/Gui/RecordingSettingsColumn.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2023 Aleix Pol Gonzalez 2 | * SPDX-FileCopyrightText: 2022 Noah Davis 3 | * SPDX-License-Identifier: LGPL-2.0-or-later 4 | */ 5 | 6 | import QtQuick 7 | import QtQuick.Layouts 8 | import QtQuick.Controls as QQC 9 | import org.kde.kirigami as Kirigami 10 | import org.kde.spectacle.private 11 | 12 | ColumnLayout { 13 | spacing: Kirigami.Units.mediumSpacing 14 | QQC.CheckBox { 15 | Layout.fillWidth: true 16 | text: i18n("Include mouse pointer") 17 | QQC.ToolTip.text: i18n("Show the mouse cursor in the screen recording.") 18 | QQC.ToolTip.delay: Kirigami.Units.toolTipDelay 19 | QQC.ToolTip.visible: hovered 20 | checked: Settings.videoIncludePointer 21 | onToggled: Settings.videoIncludePointer = checked 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /dbus/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # install the DBus service and interface files in the correct place 2 | include(ECMGenerateDBusServiceFile) 3 | include(ECMConfiguredInstall) 4 | 5 | # Generate and install dbus-activated systemd service 6 | ecm_install_configured_files(INPUT app-org.kde.spectacle.service.in DESTINATION ${KDE_INSTALL_SYSTEMDUSERUNITDIR}) 7 | 8 | ecm_generate_dbus_service_file( 9 | NAME org.kde.spectacle 10 | EXECUTABLE "${KDE_INSTALL_FULL_BINDIR}/spectacle --dbus" 11 | SYSTEMD_SERVICE app-org.kde.spectacle.service 12 | DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR} 13 | ) 14 | ecm_generate_dbus_service_file( 15 | NAME org.kde.Spectacle 16 | EXECUTABLE "${KDE_INSTALL_FULL_BINDIR}/spectacle --dbus" 17 | SYSTEMD_SERVICE app-org.kde.spectacle.service 18 | DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR} 19 | ) 20 | 21 | install(FILES org.kde.Spectacle.xml DESTINATION ${KDE_INSTALL_DBUSINTERFACEDIR}) 22 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include_directories(${PROJECT_SOURCE_DIR}/src) 2 | 3 | SET(FILENAME_TEST_SRCS 4 | FilenameTest.cpp 5 | ../src/ShortcutActions.cpp 6 | ../src/ExportManager.cpp 7 | ../src/Platforms/ImagePlatform.cpp 8 | ../src/Platforms/VideoPlatform.cpp 9 | ) 10 | 11 | ecm_qt_declare_logging_category(FILENAME_TEST_SRCS 12 | HEADER spectacle_debug.h 13 | IDENTIFIER SPECTACLE_LOG 14 | CATEGORY_NAME spectacle 15 | DESCRIPTION "spectacle (general)" 16 | EXPORT SPECTACLE 17 | ) 18 | 19 | kconfig_add_kcfg_files(FILENAME_TEST_SRCS GENERATE_MOC ${PROJECT_SOURCE_DIR}/src/Gui/SettingsDialog/settings.kcfgc) 20 | 21 | ecm_add_test( 22 | ${FILENAME_TEST_SRCS} 23 | TEST_NAME "filename_test" 24 | LINK_LIBRARIES Qt::Test 25 | Qt::PrintSupport Qt::Qml KF6::I18n KF6::ConfigCore KF6::GlobalAccel KF6::KIOCore KF6::WindowSystem KF6::XmlGui KF6::GuiAddons KF6::PrisonScanner 26 | ) 27 | -------------------------------------------------------------------------------- /.flatpak-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "org.kde.spectacle", 3 | "branch": "master", 4 | "runtime": "org.kde.Platform", 5 | "runtime-version": "5.15-22.08", 6 | "sdk": "org.kde.Sdk", 7 | "command": "spectacle", 8 | "tags": ["nightly"], 9 | "desktop-file-name-suffix": " (Nightly)", 10 | "finish-args": ["--share=ipc", "--socket=x11", "--socket=wayland", 11 | "--talk-name=org.kde.kglobalaccel", 12 | "--talk-name=org.kde.KWin"], 13 | 14 | "modules": [ 15 | { 16 | "name": "kpipewire", 17 | "buildsystem": "cmake-ninja", 18 | "sources": [ { "type": "git", "url": "https://invent.kde.org/plasma/kpipewire.git", "tag": "v5.27.5" } ] 19 | }, 20 | { 21 | "name": "spectacle", 22 | "buildsystem": "cmake-ninja", 23 | "sources": [ 24 | { "type": "dir", "path": "." } 25 | ] 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /src/Gui/SettingsDialog/SettingsUtils.h: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2023 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #pragma once 6 | 7 | #include 8 | #include 9 | 10 | /** 11 | * A small collection of functions to help prevent duplicating the implementations of custom code used by the Settings class. 12 | */ 13 | 14 | /** 15 | * Gets a translated string, but the string is only translated once. 16 | * Mainly meant to keep file paths consistent. 17 | */ 18 | inline QString onceTranslatedString(KConfigSkeleton *kcs, const QString &groupName, const char *entryName, const QString &localizedDefault) 19 | { 20 | auto config = kcs->sharedConfig(); 21 | auto group = config->group(groupName); 22 | QString entry = group.readEntry(entryName); 23 | if (entry.isEmpty()) { 24 | entry = localizedDefault; 25 | group.writeEntry(entryName, entry); 26 | config->sync(); 27 | } 28 | return entry; 29 | } 30 | -------------------------------------------------------------------------------- /src/ShortcutActions.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2019 David Redondo 3 | * 4 | * SPDX-License-Identifier: GPL-2.0-or-later 5 | */ 6 | 7 | #ifndef SHORTCUT_ACTIONS_H 8 | #define SHORTCUT_ACTIONS_H 9 | 10 | #include 11 | 12 | class ShortcutActions 13 | { 14 | public: 15 | static ShortcutActions *self(); 16 | 17 | KActionCollection *shortcutActions(); 18 | 19 | QString componentName() const; 20 | 21 | QAction *openAction() const; 22 | QAction *fullScreenAction() const; 23 | QAction *currentScreenAction() const; 24 | QAction *activeWindowAction() const; 25 | QAction *regionAction() const; 26 | QAction *windowUnderCursorAction() const; 27 | QAction *recordScreenAction() const; 28 | QAction *recordWindowAction() const; 29 | QAction *recordRegionAction() const; 30 | QAction *openWithoutScreenshotAction() const; 31 | 32 | private: 33 | ShortcutActions(); 34 | KActionCollection mActions; 35 | }; 36 | 37 | #endif 38 | -------------------------------------------------------------------------------- /src/Gui/HelpMenu.h: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #pragma once 6 | 7 | #include "SpectacleMenu.h" 8 | 9 | #include 10 | 11 | #include 12 | 13 | #include 14 | 15 | class HelpMenu : public SpectacleMenu 16 | { 17 | Q_OBJECT 18 | QML_ELEMENT 19 | QML_SINGLETON 20 | 21 | public: 22 | static HelpMenu *instance(); 23 | 24 | Q_SLOT void showAppHelp(); 25 | 26 | static HelpMenu *create(QQmlEngine *engine, QJSEngine *) 27 | { 28 | auto inst = instance(); 29 | Q_ASSERT(inst); 30 | Q_ASSERT(inst->thread() == engine->thread()); 31 | QJSEngine::setObjectOwnership(inst, QJSEngine::CppOwnership); 32 | return inst; 33 | } 34 | 35 | private: 36 | explicit HelpMenu(QWidget *parent = nullptr); 37 | Q_SLOT void onTriggered(QAction *action); 38 | const std::unique_ptr kHelpMenu; 39 | friend class HelpMenuSingleton; 40 | }; 41 | -------------------------------------------------------------------------------- /src/Gui/SettingsDialog/VideoFormatComboBox.cpp: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2024 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #include "VideoFormatComboBox.h" 6 | 7 | using namespace Qt::StringLiterals; 8 | 9 | VideoFormatComboBox::VideoFormatComboBox(VideoFormatModel *model, QWidget *parent) 10 | : QComboBox(parent) 11 | { 12 | setModel(model); 13 | setObjectName(u"kcfg_preferredVideoFormat"_s); 14 | setProperty("kcfg_property", u"currentFormat"_s); 15 | connect(this, &QComboBox::currentIndexChanged, this, &VideoFormatComboBox::currentFormatChanged); 16 | } 17 | 18 | VideoPlatform::Format VideoFormatComboBox::currentFormat() const 19 | { 20 | return currentData(VideoFormatModel::FormatRole).value(); 21 | } 22 | 23 | void VideoFormatComboBox::setCurrentFormat(VideoPlatform::Format format) 24 | { 25 | auto model = static_cast(this->model()); 26 | setCurrentIndex(model->indexOfFormat(format)); 27 | } 28 | 29 | #include "moc_VideoFormatComboBox.cpp" 30 | -------------------------------------------------------------------------------- /src/Gui/WidgetWindowUtils.h: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2023 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #pragma once 6 | 7 | #include 8 | #include 9 | 10 | /* A small collection of functions to set the transient parents of QWidgets without segfaults. 11 | * For some reason, we have to check winId() to avoid crashes. 12 | */ 13 | 14 | inline void setWidgetTransientParent(QWidget *widget, QWindow *parent) 15 | { 16 | if (widget && widget->winId() && parent && parent->winId()) { 17 | widget->windowHandle()->setTransientParent(parent); 18 | } 19 | } 20 | 21 | inline void setWidgetTransientParentToWidget(QWidget *widget, QWidget *parent) 22 | { 23 | if (widget && widget->winId() && parent && parent->winId()) { 24 | widget->windowHandle()->setTransientParent(parent->windowHandle()); 25 | } 26 | } 27 | 28 | inline QWindow *getWidgetTransientParent(QWidget *widget) 29 | { 30 | return widget && widget->winId() ? widget->windowHandle()->transientParent() : nullptr; 31 | } 32 | -------------------------------------------------------------------------------- /src/Gui/RecordingModeButtonsColumn.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2023 Aleix Pol Gonzalez 2 | * SPDX-FileCopyrightText: 2022 Noah Davis 3 | * SPDX-License-Identifier: LGPL-2.0-or-later 4 | */ 5 | 6 | import QtQuick 7 | import QtQuick.Layouts 8 | import QtQuick.Controls as QQC 9 | import org.kde.kirigami as Kirigami 10 | import org.kde.spectacle.private 11 | 12 | ColumnLayout { 13 | spacing: Kirigami.Units.mediumSpacing 14 | Repeater { 15 | model: RecordingModeModel 16 | delegate: QQC.Button { 17 | id: button 18 | Layout.fillWidth: true 19 | leftPadding: Kirigami.Units.mediumSpacing + QmlUtils.fontMetrics.descent 20 | rightPadding: Kirigami.Units.mediumSpacing + QmlUtils.fontMetrics.descent 21 | topPadding: Kirigami.Units.mediumSpacing 22 | bottomPadding: Kirigami.Units.mediumSpacing 23 | text: model.display 24 | onClicked: SpectacleCore.startRecording(model.recordingMode, Settings.videoIncludePointer) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Gui/SpectacleMenu.h: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #pragma once 6 | 7 | #include 8 | 9 | class QQuickItem; 10 | 11 | /** 12 | * This class only exists to make QMenu more usable with Qt Quick 13 | */ 14 | class SpectacleMenu : public QMenu 15 | { 16 | Q_OBJECT 17 | Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged FINAL) 18 | public: 19 | SpectacleMenu(const QString &title, QWidget *parent = nullptr); 20 | SpectacleMenu(QWidget *parent = nullptr); 21 | 22 | /** 23 | * Same as QMenu::setVisible, but it emits visibleChanged so it can be useful in QML bindings 24 | */ 25 | void setVisible(bool visible) override; 26 | 27 | /** 28 | * Popup on the specified item 29 | */ 30 | Q_INVOKABLE virtual void popup(QQuickItem *item); 31 | 32 | Q_SIGNALS: 33 | void visibleChanged(); 34 | 35 | protected: 36 | void showEvent(QShowEvent *event) override; 37 | void hideEvent(QHideEvent *) override; 38 | }; 39 | -------------------------------------------------------------------------------- /src/Gui/AnimatedLoader.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick 6 | import org.kde.kirigami as Kirigami 7 | 8 | Loader { 9 | id: loader 10 | property int animationDuration: Kirigami.Units.shortDuration 11 | active: visible 12 | visible: opacity > 0 13 | // Using states because they stay in sync better than Behavior animations 14 | state: "active" 15 | states: [ 16 | State { 17 | name: "active" 18 | PropertyChanges { 19 | target: loader 20 | opacity: 1 21 | } 22 | }, 23 | State { 24 | name: "inactive" 25 | PropertyChanges { 26 | target: loader 27 | opacity: 0 28 | } 29 | } 30 | ] 31 | transitions: Transition { 32 | NumberAnimation { 33 | property: "opacity" 34 | duration: loader.animationDuration 35 | easing.type: Easing.OutCubic 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Gui/SizeLabel.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick 6 | import QtQuick.Templates as T 7 | import org.kde.spectacle.private 8 | 9 | T.Label { 10 | id: root 11 | property size size: Qt.size(0, 0) 12 | Binding on text { 13 | value: i18nc("Size, width×height", "%1×%2", size.width, size.height) 14 | when: root.size.width > 0 && root.size.height > 0 15 | restoreMode: Binding.RestoreNone 16 | } 17 | textFormat: Text.PlainText 18 | horizontalAlignment: Text.AlignHCenter 19 | verticalAlignment: Text.AlignVCenter 20 | elide: Text.ElideNone 21 | wrapMode: Text.NoWrap 22 | color: palette.windowText 23 | background: Item { // Label implicit size is readonly, but you can still influence it via the background 24 | implicitWidth: contextWindow.dprRound(root.contentWidth + root.leftPadding + root.rightPadding) 25 | implicitHeight: contextWindow.dprRound(root.contentHeight + root.topPadding + root.bottomPadding) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Gui/EmptyPage.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick 6 | import QtQuick.Templates as T 7 | 8 | /** 9 | * This is meant to be a very basic page that behaves like most pages do, 10 | * but inherits no externally defined content or behavior. 11 | */ 12 | T.Page { 13 | // implicitHeader/FooterWidth and implicitHeader/FooterHeight are 0 when header/footer is not visible 14 | implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, 15 | contentWidth + leftPadding + rightPadding, 16 | implicitHeaderWidth, 17 | implicitFooterWidth) 18 | implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, 19 | contentHeight + topPadding + bottomPadding 20 | + (implicitHeaderHeight > 0 ? implicitHeaderHeight + spacing : 0) 21 | + (implicitFooterHeight > 0 ? implicitFooterHeight + spacing : 0)) 22 | hoverEnabled: false 23 | } 24 | -------------------------------------------------------------------------------- /src/Gui/SmartSpinBox.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2015 Boudhayan Gupta 3 | * 4 | * SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | #include 8 | #include 9 | 10 | #include "SmartSpinBox.h" 11 | 12 | SmartSpinBox::SmartSpinBox(QWidget *parent) 13 | : QDoubleSpinBox(parent) 14 | { 15 | connect(this, qOverload(&SmartSpinBox::valueChanged), this, &SmartSpinBox::suffixChangeHandler); 16 | } 17 | 18 | QString SmartSpinBox::textFromValue(double val) const 19 | { 20 | if ((qFloor(val) == val) && (qCeil(val) == val)) { 21 | return QWidget::locale().toString(qint64(val)); 22 | } 23 | return QWidget::locale().toString(val, 'f', decimals()); 24 | } 25 | 26 | void SmartSpinBox::suffixChangeHandler(double val) 27 | { 28 | int integerSeconds = static_cast(val); 29 | if (val == integerSeconds) { 30 | setSuffix(i18ncp("Integer number of seconds", " second", " seconds", integerSeconds)); 31 | } else { 32 | setSuffix(i18nc("Decimal number of seconds", " seconds")); 33 | } 34 | } 35 | 36 | #include "moc_SmartSpinBox.cpp" 37 | -------------------------------------------------------------------------------- /src/Gui/SettingsDialog/SettingsDialog.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2015 Boudhayan Gupta 3 | * 4 | * SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | #ifndef SETTINGSDIALOG_H 8 | #define SETTINGSDIALOG_H 9 | 10 | #include 11 | 12 | class GeneralOptionsPage; 13 | class ImageSaveOptionsPage; 14 | class VideoSaveOptionsPage; 15 | class ShortcutsOptionsPage; 16 | 17 | class SettingsDialog : public KConfigDialog 18 | { 19 | Q_OBJECT 20 | 21 | public: 22 | explicit SettingsDialog(QWidget *parent = nullptr); 23 | 24 | protected: 25 | QSize sizeHint() const override; 26 | void showEvent(QShowEvent *event) override; 27 | 28 | private: 29 | bool hasChanged() override; 30 | bool isDefault() override; 31 | void updateSettings() override; 32 | void updateWidgets() override; 33 | void updateWidgetsDefault() override; 34 | 35 | GeneralOptionsPage *const m_generalPage; 36 | ImageSaveOptionsPage *const m_imagesPage; 37 | VideoSaveOptionsPage *const m_videosPage; 38 | ShortcutsOptionsPage *const m_shortcutsPage; 39 | }; 40 | 41 | #endif // SETTINGSDIALOG_H 42 | -------------------------------------------------------------------------------- /src/Gui/SettingsDialog/GeneralOptionsPage.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2015 Boudhayan Gupta 3 | * 4 | * SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | #ifndef GENERALOPTIONSPAGE_H 8 | #define GENERALOPTIONSPAGE_H 9 | 10 | #include 11 | #include 12 | 13 | class Ui_GeneralOptions; 14 | class OcrLanguageSelector; 15 | 16 | class GeneralOptionsPage : public QWidget 17 | { 18 | Q_OBJECT 19 | 20 | public: 21 | explicit GeneralOptionsPage(QWidget *parent = nullptr); 22 | ~GeneralOptionsPage() override; 23 | 24 | void refreshOcrLanguageSettings(); 25 | 26 | /** 27 | * @brief Get direct access to the OCR language selector widget 28 | * @return Pointer to the OcrLanguageSelector widget for direct manipulation 29 | */ 30 | OcrLanguageSelector *ocrLanguageSelector() const 31 | { 32 | return m_ocrLanguageSelector; 33 | } 34 | 35 | Q_SIGNALS: 36 | void ocrLanguageChanged(); 37 | 38 | private: 39 | QScopedPointer m_ui; 40 | OcrLanguageSelector *m_ocrLanguageSelector; 41 | }; 42 | 43 | #endif // GENERALOPTIONSPAGE_H 44 | -------------------------------------------------------------------------------- /src/Gui/TextContextMenu.h: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2023 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #pragma once 6 | 7 | #include "SpectacleMenu.h" 8 | #include 9 | 10 | class TextContextMenu : public SpectacleMenu 11 | { 12 | Q_OBJECT 13 | QML_ELEMENT 14 | QML_SINGLETON 15 | 16 | public: 17 | static TextContextMenu *instance(); 18 | 19 | static TextContextMenu *create(QQmlEngine *engine, QJSEngine *) 20 | { 21 | auto inst = instance(); 22 | Q_ASSERT(inst); 23 | Q_ASSERT(inst->thread() == engine->thread()); 24 | QJSEngine::setObjectOwnership(inst, QJSEngine::CppOwnership); 25 | return inst; 26 | } 27 | 28 | void popup(QQuickItem *editor) override; 29 | 30 | private: 31 | explicit TextContextMenu(QWidget *parent = nullptr); 32 | 33 | QAction *undoAction = nullptr; 34 | QAction *redoAction = nullptr; 35 | QAction *cutAction = nullptr; 36 | QAction *copyAction = nullptr; 37 | QAction *pasteAction = nullptr; 38 | QAction *deleteAction = nullptr; 39 | QAction *selectAllAction = nullptr; 40 | 41 | friend class TextContextMenuSingleton; 42 | }; 43 | -------------------------------------------------------------------------------- /src/DebugUtils.h: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2024 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #pragma once 6 | 7 | #include "spectacle_debug.h" 8 | 9 | /** 10 | * Convenience functions for categorizing output 11 | */ 12 | namespace Log 13 | { 14 | using CategoryFunction = QMessageLogger::CategoryFunction; 15 | 16 | static inline auto debug(CategoryFunction category = SPECTACLE_LOG) 17 | { 18 | return QMessageLogger(nullptr, 0, nullptr).debug(category).noquote(); 19 | } 20 | 21 | static inline auto info(CategoryFunction category = SPECTACLE_LOG) 22 | { 23 | return QMessageLogger(nullptr, 0, nullptr).info(category).noquote(); 24 | } 25 | 26 | static inline auto warning(CategoryFunction category = SPECTACLE_LOG) 27 | { 28 | return QMessageLogger(nullptr, 0, nullptr).warning(category).noquote(); 29 | } 30 | 31 | static inline auto critical(CategoryFunction category = SPECTACLE_LOG) 32 | { 33 | return QMessageLogger(nullptr, 0, nullptr).critical(category).noquote(); 34 | } 35 | 36 | static inline auto fatal(CategoryFunction category = SPECTACLE_LOG) 37 | { 38 | return QMessageLogger(nullptr, 0, nullptr).fatal(category).noquote(); 39 | } 40 | 41 | }; 42 | -------------------------------------------------------------------------------- /src/Gui/NewScreenshotToolButton.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2025 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick 6 | import org.kde.kirigami as Kirigami 7 | import org.kde.spectacle.private 8 | 9 | TtToolButton { 10 | // Can't rely on checked since clicking also toggles checked 11 | readonly property bool showCancel: SpectacleCore.captureTimeRemaining > 0 12 | readonly property real cancelWidth: QmlUtils.getButtonSize(display, cancelText(Settings.captureDelay), icon.name).width 13 | 14 | function cancelText(seconds) { 15 | return i18np("Cancel (%1 second)", "Cancel (%1 seconds)", Math.ceil(seconds)) 16 | } 17 | 18 | checked: showCancel 19 | width: if (showCancel) { 20 | return cancelWidth 21 | } else { 22 | return display === TtToolButton.IconOnly ? height : implicitWidth 23 | } 24 | icon.name: showCancel ? "dialog-cancel" : "list-add" 25 | text: showCancel ? 26 | cancelText(SpectacleCore.captureTimeRemaining / 1000) 27 | : i18n("New Screenshot") 28 | onClicked: if (showCancel) { 29 | SpectacleCore.cancelScreenshot() 30 | } else { 31 | SpectacleCore.takeNewScreenshot() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.kde-ci.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: None 2 | # SPDX-License-Identifier: CC0-1.0 3 | 4 | Dependencies: 5 | - 'on': ['Linux', 'FreeBSD'] 6 | 'require': 7 | 'frameworks/extra-cmake-modules': '@latest-kf6' 8 | 'frameworks/kcoreaddons': '@latest-kf6' 9 | 'frameworks/kwidgetsaddons': '@latest-kf6' 10 | 'frameworks/kdbusaddons': '@latest-kf6' 11 | 'frameworks/knotifications': '@latest-kf6' 12 | 'frameworks/kconfig': '@latest-kf6' 13 | 'frameworks/ki18n': '@latest-kf6' 14 | 'frameworks/kio': '@latest-kf6' 15 | 'frameworks/kwindowsystem': '@latest-kf6' 16 | 'frameworks/kdoctools': '@latest-kf6' 17 | 'frameworks/kglobalaccel': '@latest-kf6' 18 | 'frameworks/kxmlgui': '@latest-kf6' 19 | 'frameworks/purpose': '@latest-kf6' 20 | 'frameworks/kirigami': '@latest-kf6' 21 | 'frameworks/kguiaddons': '@latest-kf6' 22 | 'frameworks/kcrash': '@latest-kf6' 23 | 'frameworks/kstatusnotifieritem': '@latest-kf6' 24 | 'frameworks/prison': '@latest-kf6' 25 | 'plasma/kpipewire': '@latest-kf6' 26 | 'plasma/layer-shell-qt': '@latest-kf6' 27 | 'libraries/kquickimageeditor': '@latest-kf6' 28 | 'third-party/wayland': '@latest' 29 | 30 | Options: 31 | require-passing-tests-on: ['Linux', 'FreeBSD', 'Windows'] 32 | -------------------------------------------------------------------------------- /cmake/tesseract_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | int main() 7 | { 8 | tesseract::TessBaseAPI api; 9 | 10 | if (api.Init(nullptr, nullptr) != 0) { 11 | std::cerr << "Failed to initialize Tesseract" << std::endl; 12 | return 1; 13 | } 14 | 15 | std::vector languages; 16 | api.GetAvailableLanguagesAsVector(&languages); 17 | 18 | // Filter out 'osd' as it's not a usable language for OCR 19 | std::vector usableLanguages; 20 | for (const auto &lang : languages) { 21 | if (lang != "osd") { 22 | usableLanguages.push_back(lang); 23 | } 24 | } 25 | 26 | if (usableLanguages.empty()) { 27 | std::cerr << "No usable Tesseract language packs found. Install language data files (e.g., tesseract-ocr-eng)" << std::endl; 28 | return 1; 29 | } 30 | 31 | std::cout << "Found " << usableLanguages.size() << " Tesseract language pack(s): "; 32 | for (size_t i = 0; i < usableLanguages.size(); ++i) { 33 | std::cout << usableLanguages[i]; 34 | if (i < usableLanguages.size() - 1) 35 | std::cout << ", "; 36 | } 37 | std::cout << std::endl; 38 | 39 | return 0; 40 | } 41 | -------------------------------------------------------------------------------- /src/Gui/SettingsDialog/ShortcutsOptionsPage.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2019 David Redondo 3 | * 4 | * SPDX-License-Identifier: GPL-2.0-or-later 5 | */ 6 | 7 | #include "ShortcutsOptionsPage.h" 8 | 9 | #include "ShortcutActions.h" 10 | 11 | #include 12 | 13 | #include 14 | 15 | ShortcutsOptionsPage::ShortcutsOptionsPage(QWidget *parent) 16 | : QWidget(parent) 17 | { 18 | auto mainLayout = new QVBoxLayout(this); 19 | mEditor = new KShortcutsEditor(ShortcutActions::self()->shortcutActions(), this, KShortcutsEditor::ActionType::GlobalAction); 20 | mainLayout->addWidget(mEditor); 21 | connect(mEditor, &KShortcutsEditor::keyChange, this, &ShortcutsOptionsPage::shortCutsChanged); 22 | } 23 | 24 | ShortcutsOptionsPage::~ShortcutsOptionsPage() 25 | { 26 | mEditor->undo(); 27 | } 28 | 29 | void ShortcutsOptionsPage::resetChanges() 30 | { 31 | mEditor->undo(); 32 | } 33 | 34 | void ShortcutsOptionsPage::saveChanges() 35 | { 36 | mEditor->save(); 37 | } 38 | 39 | bool ShortcutsOptionsPage::isModified() const 40 | { 41 | return mEditor->isModified(); 42 | } 43 | 44 | void ShortcutsOptionsPage::defaults() const 45 | { 46 | mEditor->allDefault(); 47 | } 48 | 49 | #include "moc_ShortcutsOptionsPage.cpp" 50 | -------------------------------------------------------------------------------- /kconf_update/spectacle-24.02.0-keep_old_filename_templates.cpp: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2023 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #include "ConfigUtils.h" 6 | #include 7 | #include 8 | 9 | using namespace Qt::StringLiterals; 10 | 11 | int main() 12 | { 13 | const auto fileName = u"spectaclerc"_s; 14 | if (!isFileOlderThanDateTime(fileName, u"2024-02-28T00:00:00Z"_s)) { 15 | return 0; 16 | } 17 | 18 | // We only need to read spectaclerc, so we use SimpleConfig. 19 | auto spectaclerc = KSharedConfig::openConfig(fileName, KConfig::SimpleConfig); 20 | 21 | // Preserve old defaults for existing users that didn't already have these set. 22 | auto imageSaveGroup = spectaclerc->group(QStringLiteral("ImageSave")); 23 | if (isEntryDefault(imageSaveGroup, "imageFilenameTemplate")) { 24 | imageSaveGroup.writeEntry("imageFilenameTemplate", "Screenshot_
_"); 25 | } 26 | 27 | auto videoSaveGroup = spectaclerc->group(QStringLiteral("VideoSave")); 28 | if (isEntryDefault(videoSaveGroup, "videoFilenameTemplate")) { 29 | videoSaveGroup.writeEntry("videoFilenameTemplate", "Screencast_
_"); 30 | } 31 | 32 | return spectaclerc->sync() ? 0 : 1; 33 | } 34 | -------------------------------------------------------------------------------- /src/VideoFormatModel.h: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #pragma once 6 | 7 | #include "Platforms/VideoPlatform.h" 8 | 9 | #include 10 | 11 | /** 12 | * This is a model containing the current supported capture modes and their labels and shortcuts. 13 | */ 14 | class VideoFormatModel : public QAbstractListModel 15 | { 16 | Q_OBJECT 17 | QML_ELEMENT 18 | Q_PROPERTY(int count READ rowCount NOTIFY countChanged FINAL) 19 | public: 20 | explicit VideoFormatModel(QObject *parent = nullptr); 21 | 22 | enum { 23 | FormatRole = Qt::UserRole + 1, 24 | ExtensionRole = Qt::UserRole + 2, 25 | }; 26 | 27 | void setFormats(VideoPlatform::Formats formats); 28 | 29 | int indexOfFormat(VideoPlatform::Format format) const; 30 | 31 | QHash roleNames() const override; 32 | QVariant data(const QModelIndex &index, int role) const override; 33 | int rowCount(const QModelIndex &parent = QModelIndex()) const override; 34 | 35 | Q_SIGNALS: 36 | void countChanged(); 37 | 38 | private: 39 | struct Item { 40 | QString label; 41 | VideoPlatform::Format format = VideoPlatform::NoFormat; 42 | QString extension = {}; 43 | }; 44 | 45 | QList m_data; 46 | QHash m_roleNames; 47 | }; 48 | -------------------------------------------------------------------------------- /src/Gui/AnnotationEditor.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2022 Marco Martin 3 | * 4 | * SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | import QtQuick 8 | import QtQuick.Layouts 9 | import org.kde.kquickimageeditor 10 | import org.kde.spectacle.private 11 | 12 | AnnotationViewport { 13 | id: root 14 | 15 | document: SpectacleCore.annotationDocument 16 | viewportRect: Qt.rect(0, 0, width, height) 17 | 18 | onPressedChanged: if (pressed) { 19 | if (textTool.shouldShow) { 20 | textTool.forceActiveFocus(Qt.MouseFocusReason); 21 | } 22 | } 23 | 24 | Item { 25 | x: -root.viewportRect.x 26 | y: -root.viewportRect.y 27 | transformOrigin: Item.TopLeft 28 | TextTool { 29 | id: textTool 30 | viewport: root 31 | } 32 | AnnotationSelectionTool { 33 | id: selectionTool 34 | viewport: root 35 | } 36 | HoverOutline { 37 | viewport: root 38 | hidden: selectionTool.hovered || selectionTool.dragging 39 | } 40 | } 41 | 42 | Shortcut { 43 | enabled: root.enabled 44 | sequences: [StandardKey.Undo] 45 | onActivated: root.document.undo() 46 | } 47 | Shortcut { 48 | enabled: root.enabled 49 | sequences: [StandardKey.Redo] 50 | onActivated: root.document.redo() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Gui/RecordOptions.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2023 Aleix Pol Gonzalez 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick 6 | import QtQuick.Layouts 7 | import QtQuick.Controls as QQC 8 | import org.kde.kirigami as Kirigami 9 | import org.kde.spectacle.private 10 | 11 | ColumnLayout { 12 | ColumnLayout { 13 | visible: !SpectacleCore.videoPlatform.isRecording 14 | spacing: Kirigami.Units.mediumSpacing 15 | 16 | RecordingModeButtonsColumn { 17 | Layout.fillWidth: true 18 | } 19 | Kirigami.Heading { 20 | Layout.fillWidth: true 21 | topPadding: -recordingSettingsMetrics.descent + parent.spacing 22 | bottomPadding: -recordingSettingsMetrics.descent + parent.spacing 23 | text: i18n("Recording Settings") 24 | level: 3 25 | FontMetrics { 26 | id: recordingSettingsMetrics 27 | } 28 | } 29 | RecordingSettingsColumn { 30 | Layout.fillWidth: true 31 | } 32 | } 33 | ColumnLayout { 34 | visible: SpectacleCore.videoPlatform.isRecording 35 | QQC.Button { 36 | Layout.fillWidth: true 37 | text: i18n("Finish recording") 38 | onClicked: SpectacleCore.finishRecording() 39 | } 40 | } 41 | Item { 42 | Layout.fillHeight: true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LICENSES/BSD-3-Clause.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) . All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spectacle - The KDE Screenshot Utility 2 | 3 | Spectacle is a screenshot taking utility for the KDE desktop. Spectacle 4 | can also be used in non-KDE X11 desktop environments. 5 | 6 | ![Screenshot of Spectacle](https://cdn.kde.org/screenshots/spectacle/spectacle.png) 7 | 8 | ## Get help 9 | You can get help in : 10 | * Forum: https://discuss.kde.org/tag/spectacle 11 | * Matrix: https://matrix.to/#/#kde:kde.org 12 | * IRC: irc://irc.libera.chat/kde 13 | ## Contributing 14 | 15 | Spectacle is developed under the KDE umbrella and uses KDE infrastructure 16 | for development. 17 | 18 | Please see the file [`CONTRIBUTING`](./CONTRIBUTING.md) for details on coding style and how 19 | to contribute patches. Please note that pull requests on GitHub aren't 20 | supported. The recommended way of contributing patches is via KDE's 21 | instance of GitLab at https://invent.kde.org/plasma/spectacle. 22 | 23 | ## Release Schedule 24 | 25 | Spectacle is released by KDE's release service and has three 26 | major releases every year. They are numbered YY.MM, where YY is the two- 27 | digit year and MM is the two-digit month. Major releases are made in April, 28 | August and December every year. The Spectacle version follows the KDE 29 | release service version. 30 | 31 | ## Reporting Bugs 32 | 33 | Please report bugs at KDE's Bugzilla, available at https://bugs.kde.org/. 34 | 35 | For discussions, the `#kde-devel` IRC channel and the kde-devel mailing list 36 | are good places to post. 37 | -------------------------------------------------------------------------------- /kconf_update/spectacle-24.02.0-keep_old_save_location.cpp: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2023 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #include "ConfigUtils.h" 6 | #include 7 | #include 8 | #include 9 | 10 | using namespace Qt::StringLiterals; 11 | 12 | int main() 13 | { 14 | const auto fileName = u"spectaclerc"_s; 15 | if (!isFileOlderThanDateTime(fileName, u"2024-02-28T00:00:00Z"_s)) { 16 | return 0; 17 | } 18 | 19 | // We only need to read spectaclerc, so we use SimpleConfig. 20 | auto spectaclerc = KSharedConfig::openConfig(fileName, KConfig::SimpleConfig); 21 | 22 | // Preserve old defaults for existing users that didn't already have these set. 23 | auto imageSaveGroup = spectaclerc->group(QStringLiteral("ImageSave")); 24 | if (isEntryDefault(imageSaveGroup, "imageSaveLocation")) { 25 | const auto url = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::PicturesLocation) + u'/'); 26 | imageSaveGroup.writeEntry("imageSaveLocation", url); 27 | } 28 | 29 | auto videoSaveGroup = spectaclerc->group(QStringLiteral("VideoSave")); 30 | if (isEntryDefault(videoSaveGroup, "videoSaveLocation")) { 31 | const auto url = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::MoviesLocation) + u'/'); 32 | videoSaveGroup.writeEntry("videoSaveLocation", url); 33 | } 34 | 35 | return spectaclerc->sync() ? 0 : 1; 36 | } 37 | -------------------------------------------------------------------------------- /src/SpectacleDBusAdapter.h: -------------------------------------------------------------------------------- 1 | /* This file is part of Spectacle, the KDE screenshot utility 2 | * SPDX-FileCopyrightText: 2015 Boudhayan Gupta 3 | * SPDX-License-Identifier: LGPL-2.0-or-later 4 | */ 5 | 6 | #pragma once 7 | 8 | #include "SpectacleCore.h" 9 | #include 10 | 11 | class SpectacleDBusAdapter : public QDBusAbstractAdaptor 12 | { 13 | Q_OBJECT 14 | Q_CLASSINFO("D-Bus Interface", "org.kde.Spectacle") 15 | public: 16 | SpectacleDBusAdapter(SpectacleCore *parent); 17 | ~SpectacleDBusAdapter() override = default; 18 | 19 | inline SpectacleCore *parent() const; 20 | 21 | public Q_SLOTS: 22 | 23 | Q_NOREPLY void FullScreen(int includeMousePointer); 24 | Q_NOREPLY void CurrentScreen(int includeMousePointer); 25 | Q_NOREPLY void ActiveWindow(int includeWindowDecorations, int includeMousePointer, int includeWindowShadow); 26 | Q_NOREPLY void WindowUnderCursor(int includeWindowDecorations, int includeMousePointer, int includeWindowShadow); 27 | Q_NOREPLY void RectangularRegion(int includeMousePointer); 28 | Q_NOREPLY void RecordRegion(int includeMousePointer); 29 | Q_NOREPLY void RecordScreen(int includeMousePointer); 30 | Q_NOREPLY void RecordWindow(int includeMousePointer); 31 | Q_NOREPLY void OpenWithoutScreenshot(); 32 | 33 | Q_SIGNALS: 34 | 35 | void ScreenshotTaken(const QString &fileName); 36 | void ScreenshotFailed(const QString &message); 37 | void RecordingTaken(const QString &fileName); 38 | void RecordingFailed(const QString &message); 39 | }; 40 | -------------------------------------------------------------------------------- /src/Platforms/VideoPlatformWayland.h: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2023 Aleix Pol Gonzalez 3 | 4 | SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | #pragma once 8 | 9 | #include "VideoPlatform.h" 10 | #include 11 | #include 12 | #include 13 | 14 | class Screencasting; 15 | 16 | /** 17 | * The VideoPlatformWayland class uses the org.kde.KWin.ScreenShot2 dbus interface 18 | * for taking screenshots of screens and windows. 19 | */ 20 | class VideoPlatformWayland final : public VideoPlatform 21 | { 22 | Q_OBJECT 23 | 24 | public: 25 | VideoPlatformWayland(QObject *parent = nullptr); 26 | 27 | RecordingModes supportedRecordingModes() const override; 28 | Formats supportedFormats() const override; 29 | void startRecording(const QUrl &fileUrl, RecordingMode recordingMode, const QVariantMap &options, bool includePointer) override; 30 | void finishRecording() override; 31 | 32 | Format formatForEncoder(PipeWireBaseEncodedStream::Encoder encoder) const; 33 | PipeWireBaseEncodedStream::Encoder encoderForFormat(Format format) const; 34 | 35 | protected: 36 | void timerEvent(QTimerEvent *event) override; 37 | 38 | private: 39 | void initialize(); 40 | bool mkDirPath(const QUrl &fileUrl); 41 | void selectAndRecord(const QUrl &fileUrl, RecordingMode recordingMode, bool includePointer); 42 | 43 | Screencasting *const m_screencasting; 44 | std::unique_ptr m_recorder; 45 | QFuture m_recorderFuture; 46 | int m_frameBytes; 47 | QBasicTimer m_memoryTimer; 48 | }; 49 | -------------------------------------------------------------------------------- /src/Platforms/PlatformNull.h: -------------------------------------------------------------------------------- 1 | /* This file is part of Spectacle, the KDE screenshot utility 2 | * SPDX-FileCopyrightText: 2019 Boudhayan Gupta 3 | * SPDX-License-Identifier: LGPL-2.0-or-later 4 | */ 5 | 6 | #pragma once 7 | 8 | #include "ImagePlatform.h" 9 | #include "VideoPlatform.h" 10 | 11 | class ImagePlatformNull final : public ImagePlatform 12 | { 13 | Q_OBJECT 14 | 15 | public: 16 | explicit ImagePlatformNull(QObject *parent = nullptr); 17 | ~ImagePlatformNull() override = default; 18 | 19 | GrabModes supportedGrabModes() const override final; 20 | ShutterModes supportedShutterModes() const override final; 21 | 22 | public Q_SLOTS: 23 | 24 | void doGrab(ImagePlatform::ShutterMode shutterMode, 25 | ImagePlatform::GrabMode grabMode, 26 | bool includePointer, 27 | bool includeDecorations, 28 | bool includeShadow) override final; 29 | }; 30 | 31 | // A default video platform implementation. Can be used for platforms that aren't supported. 32 | class VideoPlatformNull final : public VideoPlatform 33 | { 34 | Q_OBJECT 35 | 36 | public: 37 | explicit VideoPlatformNull(const QString &unavailableMessage = {}, QObject *parent = nullptr); 38 | 39 | RecordingModes supportedRecordingModes() const override; 40 | Formats supportedFormats() const override; 41 | void startRecording(const QUrl &fileUrl, RecordingMode recordingMode, const QVariantMap &options, bool includePointer) override; 42 | void finishRecording() override; 43 | 44 | private: 45 | QString m_unavailableMessage; 46 | }; 47 | -------------------------------------------------------------------------------- /kconf_update/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # add the spectacle.upd file 2 | install(FILES spectacle.upd DESTINATION ${KDE_INSTALL_KCONFUPDATEDIR}) 3 | 4 | # add the scripts 5 | add_executable(spectacle-24.02.0-video_format "spectacle-24.02.0-video_format.cpp") 6 | target_link_libraries(spectacle-24.02.0-video_format 7 | KF6::ConfigCore 8 | KF6::XmlGui 9 | ) 10 | 11 | add_executable(spectacle-24.02.0-keep_old_save_location "spectacle-24.02.0-keep_old_save_location.cpp") 12 | target_link_libraries(spectacle-24.02.0-keep_old_save_location 13 | KF6::ConfigCore 14 | KF6::XmlGui 15 | ) 16 | 17 | add_executable(spectacle-24.02.0-rename_settings "spectacle-24.02.0-rename_settings.cpp") 18 | target_link_libraries(spectacle-24.02.0-rename_settings 19 | KF6::ConfigCore 20 | KF6::XmlGui 21 | ) 22 | 23 | add_executable(spectacle-24.02.0-keep_old_filename_templates "spectacle-24.02.0-keep_old_filename_templates.cpp") 24 | target_link_libraries(spectacle-24.02.0-keep_old_filename_templates 25 | KF6::ConfigCore 26 | KF6::XmlGui 27 | ) 28 | 29 | add_executable(spectacle-24.02.0-change_placeholder_format "spectacle-24.02.0-change_placeholder_format.cpp") 30 | target_link_libraries(spectacle-24.02.0-change_placeholder_format 31 | KF6::ConfigCore 32 | KF6::XmlGui 33 | ) 34 | 35 | # install C++ scripts to kconf_update_bin 36 | install( 37 | TARGETS 38 | spectacle-24.02.0-video_format 39 | spectacle-24.02.0-keep_old_save_location 40 | spectacle-24.02.0-rename_settings 41 | spectacle-24.02.0-keep_old_filename_templates 42 | spectacle-24.02.0-change_placeholder_format 43 | DESTINATION ${KDE_INSTALL_LIBDIR}/kconf_update_bin 44 | ) 45 | -------------------------------------------------------------------------------- /src/Gui/OptionsMenu.h: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #ifndef OPTIONSMENU_H 6 | #define OPTIONSMENU_H 7 | 8 | #include "SpectacleMenu.h" 9 | #include "Gui/SmartSpinBox.h" 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | /** 17 | * A menu that allows choosing capture modes and related options. 18 | */ 19 | class OptionsMenu : public SpectacleMenu 20 | { 21 | Q_OBJECT 22 | QML_ELEMENT 23 | QML_SINGLETON 24 | 25 | public: 26 | static OptionsMenu *instance(); 27 | 28 | Q_SLOT void showPreferencesDialog(); 29 | 30 | static OptionsMenu *create(QQmlEngine *engine, QJSEngine *) 31 | { 32 | auto inst = instance(); 33 | Q_ASSERT(inst); 34 | Q_ASSERT(inst->thread() == engine->thread()); 35 | QJSEngine::setObjectOwnership(inst, QJSEngine::CppOwnership); 36 | return inst; 37 | } 38 | 39 | protected: 40 | void changeEvent(QEvent *event) override; 41 | void keyPressEvent(QKeyEvent *event) override; 42 | void mouseReleaseEvent(QMouseEvent *event) override; 43 | 44 | explicit OptionsMenu(QWidget *parent = nullptr); 45 | 46 | void delayActionLayoutUpdate(); 47 | const std::unique_ptr m_delayAction; 48 | const std::unique_ptr m_delayWidget; 49 | const std::unique_ptr m_delayLayout; 50 | const std::unique_ptr m_delayLabel; 51 | const std::unique_ptr m_delaySpinBox; 52 | bool m_updatingDelayActionLayout = false; 53 | }; 54 | 55 | #endif // OPTIONSMENU_H 56 | -------------------------------------------------------------------------------- /src/ScreenShotEffect.cpp: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2023 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #include "ScreenShotEffect.h" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | using namespace Qt::StringLiterals; 16 | 17 | static const auto s_kwinService = u"org.kde.KWin"_s; 18 | static const auto s_effectsObjectPath = u"/Effects"_s; 19 | static const auto s_effectsInterface = u"org.kde.kwin.Effects"_s; 20 | 21 | static const auto s_screenShot2Service = u"org.kde.KWin.ScreenShot2"_s; 22 | static const auto s_screenShot2ObjectPath = u"/org/kde/KWin/ScreenShot2"_s; 23 | static const auto s_screenShot2Interface = u"org.kde.KWin.ScreenShot2"_s; 24 | 25 | static quint32 s_version = ScreenShotEffect::NullVersion; 26 | 27 | bool ScreenShotEffect::isLoaded() 28 | { 29 | return QDBusConnection::sessionBus().interface()->isServiceRegistered(s_screenShot2Service); 30 | } 31 | 32 | quint32 ScreenShotEffect::version() 33 | { 34 | if (!QDBusConnection::sessionBus().interface()->isServiceRegistered(s_screenShot2Service)) { 35 | s_version = ScreenShotEffect::NullVersion; 36 | } else if (s_version == ScreenShotEffect::NullVersion) { 37 | QDBusInterface interface(s_screenShot2Service, s_screenShot2ObjectPath, s_screenShot2Interface); 38 | bool ok; 39 | auto version = interface.property("Version").toUInt(&ok); 40 | s_version = ok ? version : ScreenShotEffect::NullVersion; 41 | } 42 | return s_version; 43 | } 44 | -------------------------------------------------------------------------------- /kconf_update/spectacle-24.02.0-video_format.cpp: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2023 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #include "ConfigUtils.h" 6 | #include 7 | #include 8 | 9 | using namespace Qt::StringLiterals; 10 | 11 | int main() 12 | { 13 | // We only need to read spectaclerc, so we use SimpleConfig. 14 | auto spectaclerc = KSharedConfig::openConfig("spectaclerc"_L1, KConfig::SimpleConfig); 15 | 16 | // Remove old settings. 17 | spectaclerc->group(QStringLiteral("GuiConfig")).deleteEntry("videoFormat"); 18 | auto saveGroup = spectaclerc->group(QStringLiteral("Save")); 19 | // These couldn't be changed via the GUI, but removing them anyway just in case 20 | saveGroup.deleteEntry("defaultVideoSaveLocation"); 21 | saveGroup.deleteEntry("defaultSaveVideoFormat"); 22 | saveGroup.deleteEntry("saveVideoFormat"); 23 | 24 | // Copy to new groups and remove old groups 25 | auto imageSaveGroup = spectaclerc->group(QStringLiteral("ImageSave")); 26 | saveGroup.copyTo(&imageSaveGroup); 27 | saveGroup.deleteGroup(); 28 | 29 | // Rename settings 30 | KeyMap oldNewMap{ 31 | {"defaultSaveLocation", "imageSaveLocation"}, 32 | {"compressionQuality", "imageCompressionQuality"}, 33 | {"defaultSaveImageFormat", "preferredImageFormat"}, 34 | {"saveFilenameFormat", "imageFilenameFormat"}, 35 | {"lastSaveLocation", "lastImageSaveLocation"}, 36 | {"lastSaveAsLocation", "lastImageSaveAsLocation"}, 37 | }; 38 | replaceEntryKeys(imageSaveGroup, oldNewMap); 39 | 40 | return spectaclerc->sync() ? 0 : 1; 41 | } 42 | -------------------------------------------------------------------------------- /src/Gui/CaptureWindow.h: -------------------------------------------------------------------------------- 1 | /* This file is part of Spectacle, the KDE screenshot utility 2 | * SPDX-FileCopyrightText: 2015 Boudhayan Gupta 3 | * SPDX-FileCopyrightText: 2022 Noah Davis 4 | * SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | #pragma once 8 | 9 | #include "Gui/SpectacleWindow.h" 10 | 11 | class CaptureWindowPrivate; 12 | 13 | /** 14 | * The window class used for fullscreen capture UIs. 15 | */ 16 | class CaptureWindow : public SpectacleWindow 17 | { 18 | Q_OBJECT 19 | Q_PROPERTY(QScreen *screenToFollow READ screenToFollow NOTIFY screenToFollowChanged FINAL) 20 | 21 | public: 22 | enum Mode { 23 | Image, 24 | Video, 25 | }; 26 | 27 | using UniquePointer = std::unique_ptr; 28 | 29 | static UniquePointer makeUnique(Mode mode, QScreen *screen, QQmlEngine *engine, QWindow *parent = nullptr); 30 | 31 | static QList instances(); 32 | 33 | QScreen *screenToFollow() const; 34 | 35 | public Q_SLOTS: 36 | bool accept(); 37 | void cancel(); 38 | void save() override; 39 | void saveAs() override; 40 | void copyImage() override; 41 | void copyLocation() override; 42 | 43 | Q_SIGNALS: 44 | void screenToFollowChanged(); 45 | 46 | protected: 47 | void mousePressEvent(QMouseEvent *event) override; 48 | void showEvent(QShowEvent *event) override; 49 | 50 | private: 51 | explicit CaptureWindow(Mode mode, QScreen *screen, QQmlEngine *engine, QWindow *parent = nullptr); 52 | ~CaptureWindow(); 53 | 54 | void setMode(CaptureWindow::Mode mode); 55 | void syncGeometryWithScreen(); 56 | 57 | QPointer m_screenToFollow; 58 | static QList s_captureWindowInstances; 59 | }; 60 | -------------------------------------------------------------------------------- /src/Gui/FloatingToolBar.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick 6 | import QtQuick.Templates as T 7 | import org.kde.kirigami as Kirigami 8 | import org.kde.spectacle.private 9 | 10 | T.Pane { 11 | id: root 12 | property real radius: Kirigami.Units.mediumSpacing / 2 + background.border.width 13 | property real topLeftRadius: radius 14 | property real topRightRadius: radius 15 | property real bottomLeftRadius: radius 16 | property real bottomRightRadius: radius 17 | property real backgroundColorOpacity: 0.95 18 | 19 | implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, 20 | contentWidth + leftPadding + rightPadding) 21 | implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, 22 | contentHeight + topPadding + bottomPadding) 23 | 24 | padding: Kirigami.Units.mediumSpacing 25 | spacing: Kirigami.Units.mediumSpacing 26 | 27 | background: FloatingBackground { 28 | color: Qt.rgba(root.palette.window.r, 29 | root.palette.window.g, 30 | root.palette.window.b, root.backgroundColorOpacity) 31 | border.color: Qt.rgba(root.palette.windowText.r, 32 | root.palette.windowText.g, 33 | root.palette.windowText.b, 0.2) 34 | radius: root.radius 35 | corners.topLeftRadius: root.topLeftRadius 36 | corners.topRightRadius: root.topRightRadius 37 | corners.bottomLeftRadius: root.bottomLeftRadius 38 | corners.bottomRightRadius: root.bottomRightRadius 39 | border.width: contextWindow.dprRound(1) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Gui/CaptureOptions.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick 6 | import QtQuick.Layouts 7 | import org.kde.kirigami as Kirigami 8 | import org.kde.spectacle.private 9 | 10 | Column { 11 | spacing: Kirigami.Units.mediumSpacing 12 | Kirigami.Heading { 13 | anchors.left: parent.left 14 | width: Math.max(implicitWidth, parent.width) 15 | topPadding: -captureHeadingMetrics.descent 16 | bottomPadding: -captureHeadingMetrics.descent + parent.spacing 17 | text: i18n("Take a new screenshot") 18 | horizontalAlignment: Text.AlignLeft 19 | verticalAlignment: Text.AlignVCenter 20 | level: 3 21 | // If recording is supported, there would be a tab bar right above this 22 | // label with largely the same text, creating redundancy. 23 | visible: !SpectacleCore.videoPlatform.supportedRecordingModes 24 | FontMetrics { 25 | id: captureHeadingMetrics 26 | } 27 | } 28 | CaptureModeButtonsColumn { 29 | anchors.left: parent.left 30 | width: Math.max(implicitWidth, parent.width) 31 | } 32 | Kirigami.Heading { 33 | anchors.left: parent.left 34 | width: Math.max(implicitWidth, parent.width) 35 | topPadding: -captureHeadingMetrics.descent + parent.spacing 36 | bottomPadding: -captureHeadingMetrics.descent + parent.spacing 37 | horizontalAlignment: Text.AlignLeft 38 | verticalAlignment: Text.AlignVCenter 39 | text: i18nc("@title:group", "Screenshot Settings") 40 | level: 3 41 | } 42 | CaptureSettingsColumn { 43 | anchors.left: parent.left 44 | width: Math.max(Layout.minimumWidth, parent.width) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/RecordingModeModel.h: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #pragma once 6 | 7 | #include "Platforms/VideoPlatform.h" 8 | 9 | #include 10 | #include 11 | 12 | class RecordingModeModel : public QAbstractListModel 13 | { 14 | Q_OBJECT 15 | QML_ELEMENT 16 | QML_SINGLETON 17 | Q_PROPERTY(int count READ rowCount NOTIFY countChanged FINAL) 18 | public: 19 | explicit RecordingModeModel(QObject *parent = nullptr); 20 | 21 | static RecordingModeModel *instance(); 22 | 23 | static RecordingModeModel *create(QQmlEngine *engine, QJSEngine *) 24 | { 25 | auto inst = instance(); 26 | Q_ASSERT(inst); 27 | Q_ASSERT(inst->thread() == engine->thread()); 28 | QJSEngine::setObjectOwnership(inst, QJSEngine::CppOwnership); 29 | return inst; 30 | } 31 | 32 | enum { 33 | RecordingModeRole = Qt::UserRole + 1, 34 | }; 35 | 36 | QHash roleNames() const override; 37 | QVariant data(const QModelIndex &index, int role) const override; 38 | int rowCount(const QModelIndex &parent = QModelIndex()) const override; 39 | 40 | int indexOfRecordingMode(VideoPlatform::RecordingMode mode) const; 41 | 42 | void setRecordingModes(VideoPlatform::RecordingModes modes); 43 | 44 | static QString recordingModeLabel(VideoPlatform::RecordingMode mode); 45 | 46 | Q_SIGNALS: 47 | void countChanged(); 48 | void recordingModesChanged(); 49 | 50 | private: 51 | struct Item { 52 | VideoPlatform::RecordingMode mode; 53 | QString label; 54 | }; 55 | 56 | QList m_data; 57 | QHash m_roleNames; 58 | const VideoPlatform::RecordingModes m_modes; 59 | }; 60 | -------------------------------------------------------------------------------- /src/Gui/ExportMenu.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2015 Boudhayan Gupta 3 | * 4 | * SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | #ifndef EXPORTMENU_H 8 | #define EXPORTMENU_H 9 | 10 | #include "SpectacleMenu.h" 11 | 12 | #include 13 | #include 14 | 15 | #include "Config.h" 16 | #include "ExportManager.h" 17 | 18 | #ifdef PURPOSE_FOUND 19 | #include 20 | #include 21 | #include 22 | #endif 23 | 24 | class ExportMenu : public SpectacleMenu 25 | { 26 | Q_OBJECT 27 | QML_ELEMENT 28 | QML_SINGLETON 29 | 30 | public: 31 | static ExportMenu *instance(); 32 | 33 | static ExportMenu *create(QQmlEngine *engine, QJSEngine *) 34 | { 35 | auto inst = instance(); 36 | Q_ASSERT(inst); 37 | Q_ASSERT(inst->thread() == engine->thread()); 38 | QJSEngine::setObjectOwnership(inst, QJSEngine::CppOwnership); 39 | return inst; 40 | } 41 | 42 | public Q_SLOTS: 43 | void openPrintDialog(); 44 | 45 | Q_SIGNALS: 46 | void imageShared(int error, const QString &message); 47 | 48 | private: 49 | explicit ExportMenu(QWidget *parent = nullptr); 50 | 51 | Q_SLOT void onImageChanged(); 52 | Q_SLOT void openScreenshotsFolder(); 53 | Q_SLOT void buildOcrLanguageSubmenu(); 54 | Q_SLOT void triggerExtraction(const QString &languageCode); 55 | 56 | void getKServiceItems(); 57 | void createOcrLanguageSubmenu(); 58 | 59 | #ifdef PURPOSE_FOUND 60 | void loadPurposeMenu(); 61 | void loadPurposeItems(); 62 | 63 | bool mUpdatedImageAvailable; 64 | std::unique_ptr mPurposeMenu; 65 | #endif 66 | QMenu *m_ocrLanguageMenu = nullptr; 67 | friend class ExportMenuSingleton; 68 | }; 69 | 70 | #endif // EXPORTMENU_H 71 | -------------------------------------------------------------------------------- /src/Gui/ViewerWindow.h: -------------------------------------------------------------------------------- 1 | /* This file is part of Spectacle, the KDE screenshot utility 2 | * SPDX-FileCopyrightText: 2015 Boudhayan Gupta 3 | * SPDX-FileCopyrightText: 2022 Noah Davis 4 | * SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | #pragma once 8 | 9 | #include "Gui/SpectacleWindow.h" 10 | #include 11 | 12 | class ViewerWindowPrivate; 13 | 14 | /** 15 | * The window used for viewing media after it has been accepted or finished recording. 16 | * This has to be a separate window from the selection/capture window because reusing 17 | * the same window and changing the flags doesn't work nicely on Wayland. For example, 18 | * the window uses default window decorations instead of normal decorations. 19 | */ 20 | class ViewerWindow : public SpectacleWindow 21 | { 22 | Q_OBJECT 23 | public: 24 | enum Mode { 25 | Dialog, 26 | Viewer, 27 | }; 28 | 29 | using UniquePointer = std::unique_ptr; 30 | 31 | static UniquePointer makeUnique(Mode mode, QQmlEngine *engine, QWindow *parent = nullptr); 32 | 33 | static ViewerWindow *instance(); 34 | 35 | Q_INVOKABLE void startDrag(); 36 | 37 | protected: 38 | bool event(QEvent *event) override; 39 | void resizeEvent(QResizeEvent *event) override; 40 | 41 | private: 42 | explicit ViewerWindow(Mode mode, QQmlEngine *engine, QWindow *parent = nullptr); 43 | ~ViewerWindow(); 44 | 45 | void setMode(ViewerWindow::Mode mode); 46 | Q_SLOT void updateColor(); 47 | Q_SLOT void updateMinimumSize(); 48 | 49 | void setBackgroundColorRole(QPalette::ColorRole role); 50 | 51 | bool m_pixmapExists = false; 52 | QPalette::ColorRole m_backgroundColorRole; 53 | Qt::WindowStates m_oldWindowStates; 54 | const Mode m_mode; 55 | static ViewerWindow *s_viewerWindowInstance; 56 | }; 57 | -------------------------------------------------------------------------------- /src/Gui/ButtonGrid.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick 6 | import QtQuick.Controls as QQC 7 | import org.kde.kirigami as Kirigami 8 | import org.kde.spectacle.private 9 | 10 | Grid { 11 | id: root 12 | property int displayMode: QQC.AbstractButton.TextBesideIcon 13 | property int focusPolicy: Qt.StrongFocus 14 | readonly property bool mirrored: effectiveLayoutDirection === Qt.RightToLeft 15 | property bool animationsEnabled: false 16 | 17 | clip: childrenRect.width > width || childrenRect.height > height 18 | horizontalItemAlignment: Grid.AlignHCenter 19 | verticalItemAlignment: Grid.AlignVCenter 20 | spacing: Kirigami.Units.mediumSpacing 21 | /* Using -1 for either rows or columns sets the amount to unlimited, 22 | * but not if you set both to -1. Using `visibleChildren.length` to set 23 | * unlimited rows or columns can generate errors about not having enough 24 | * rows/columns when a child item's `visible` property is toggled. 25 | * Internally, rows and columns are set to defaults like this: 26 | * if (rows <= 0 && columns <= 0) { columns = 4; rows = (numVisible+3)/4; } 27 | * else if (rows <= 0) { rows = (numVisible+(columns-1))/columns; } 28 | * else if (columns <= 0) { columns = (numVisible+(rows-1))/rows; } 29 | */ 30 | columns: flow === Grid.LeftToRight ? -1 : 1 31 | rows: flow === Grid.TopToBottom ? -1 : 1 32 | move: Transition { 33 | enabled: root.animationsEnabled 34 | NumberAnimation { properties: "x,y"; duration: Kirigami.Units.longDuration; easing.type: Easing.OutCubic } 35 | } 36 | add: Transition { 37 | enabled: root.animationsEnabled 38 | NumberAnimation { properties: "x,y"; duration: Kirigami.Units.longDuration; easing.type: Easing.OutCubic } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Gui/UndoRedoGroup.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick 6 | import QtQuick.Controls as QQC 7 | import org.kde.kirigami as Kirigami 8 | import org.kde.spectacle.private 9 | import org.kde.kquickimageeditor 10 | 11 | Grid { 12 | id: root 13 | property int focusPolicy: Qt.StrongFocus 14 | property real buttonHeight: undoButton.implicitHeight 15 | property bool animationsEnabled: true 16 | spacing: Kirigami.Units.mediumSpacing 17 | columns: flow === Grid.LeftToRight ? visibleChildren.length : 1 18 | rows: flow === Grid.TopToBottom ? visibleChildren.length : 1 19 | 20 | add: Transition { 21 | enabled: root.animationsEnabled 22 | NumberAnimation { properties: "x,y"; duration: Kirigami.Units.longDuration; easing.type: Easing.OutCubic } 23 | } 24 | 25 | TtToolButton { 26 | id: undoButton 27 | enabled: SpectacleCore.annotationDocument.undoStackDepth > 0 28 | height: root.buttonHeight 29 | focusPolicy: root.focusPolicy 30 | display: QQC.ToolButton.IconOnly 31 | text: i18n("Undo") 32 | icon.name: "edit-undo" 33 | autoRepeat: true 34 | onClicked: SpectacleCore.annotationDocument.undo() 35 | } 36 | 37 | TtToolButton { 38 | enabled: SpectacleCore.annotationDocument.redoStackDepth > 0 39 | height: root.buttonHeight 40 | focusPolicy: root.focusPolicy 41 | display: QQC.ToolButton.IconOnly 42 | text: i18n("Redo") 43 | icon.name: "edit-redo" 44 | autoRepeat: true 45 | onClicked: SpectacleCore.annotationDocument.redo() 46 | } 47 | 48 | QQC.ToolSeparator { 49 | height: root.flow === Grid.TopToBottom ? implicitWidth : parent.height 50 | width: root.flow === Grid.TopToBottom ? parent.width : implicitWidth 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Gui/DashedOutline.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick 6 | import QtQuick.Shapes 7 | import org.kde.kirigami as Kirigami 8 | import org.kde.spectacle.private 9 | 10 | Outline { 11 | id: root 12 | property alias dashColor: dashPath.strokeColor 13 | property alias dashCapStyle: dashPath.capStyle 14 | property alias dashJoinStyle: dashPath.joinStyle 15 | // dashPattern is a list of alternating dash and space lengths. 16 | // Length in logical pixels is length * strokeWidth, 17 | // so divide by strokeWidth if you want to set length in logical pixels. 18 | property alias dashPattern: dashPath.dashPattern 19 | property alias dashOffset: dashPath.dashOffset 20 | property alias dashSvgPath: dashPathSvg.path 21 | property alias dashPathScale: dashPath.scale 22 | property alias dashPathHints: dashPath.pathHints 23 | 24 | // A regular alternative pattern with a spacing in logical pixels 25 | function regularDashPattern(spacing, strokeWidth = root.strokeWidth) { 26 | return [spacing / strokeWidth, spacing / strokeWidth] 27 | } 28 | 29 | ShapePath { 30 | id: dashPath 31 | fillColor: "transparent" 32 | strokeWidth: root.strokeWidth 33 | strokeColor: palette.base 34 | strokeStyle: ShapePath.DashLine 35 | dashPattern: regularDashPattern(Kirigami.Units.mediumSpacing) 36 | dashOffset: 0 37 | // FlatCap ensures that dash and space length are equal. 38 | // With other cap styles, subtract strokeWidth * 2 from the logical pixel length of dashes. 39 | capStyle: ShapePath.FlatCap 40 | joinStyle: root.joinStyle 41 | scale: root.pathScale 42 | pathHints: root.pathHints 43 | PathSvg { 44 | id: dashPathSvg 45 | path: root.svgPath 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Platforms/screencasting.h: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez 3 | 4 | SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 5 | */ 6 | 7 | #pragma once 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | class QScreen; 15 | struct zkde_screencast_unstable_v1; 16 | 17 | class ScreencastingPrivate; 18 | class ScreencastingSourcePrivate; 19 | class ScreencastingStreamPrivate; 20 | class ScreencastingStream : public QObject 21 | { 22 | Q_OBJECT 23 | public: 24 | ScreencastingStream(QObject *parent); 25 | ~ScreencastingStream() override; 26 | 27 | quint32 nodeId() const; 28 | 29 | Q_SIGNALS: 30 | void created(quint32 nodeid); 31 | void failed(const QString &error); 32 | void closed(); 33 | 34 | private: 35 | friend class Screencasting; 36 | QScopedPointer d; 37 | }; 38 | 39 | class Screencasting : public QObject 40 | { 41 | Q_OBJECT 42 | public: 43 | explicit Screencasting(QObject *parent = nullptr); 44 | ~Screencasting() override; 45 | 46 | enum CursorMode { 47 | Hidden = 1, 48 | Embedded = 2, 49 | Metadata = 4, 50 | }; 51 | Q_ENUM(CursorMode) 52 | bool isAvailable() const; 53 | bool isRegionAutoScaleSupported() const; 54 | 55 | ScreencastingStream *createOutputStream(QScreen *screen, CursorMode mode); 56 | ScreencastingStream *createRegionStream(const QRect ®ion, qreal scaling, CursorMode mode); 57 | ScreencastingStream *createWindowStream(const QString &uuid, CursorMode mode); 58 | ScreencastingStream *createVirtualMonitorStream(const QString &name, const QSize &size, qreal scale, CursorMode mode); 59 | 60 | void destroy(); 61 | 62 | Q_SIGNALS: 63 | void initialized(); 64 | void removed(); 65 | void sourcesChanged(); 66 | 67 | private: 68 | QScopedPointer d; 69 | }; 70 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | ## Commit Policy 4 | 5 | Everybody is welcome to committing small fixes and one-liners 6 | without prior notification to the maintainer, provided that the 7 | following rules are followed: 8 | 9 | 1. Please keep your commits as small and as atomic as possible. 10 | 2. Do not push both formatting and code changes in the same 11 | commit. 12 | 3. Do not fix coding style and code issues in the same commit. 13 | 14 | For larger commits, please use GitLab or send 15 | and e-mail to the maintainer. A rule of thumb to check whether your 16 | commit is a major commit is if it affects more than 5 lines of code. 17 | 18 | Break down larger fixes into smaller commits. Even if you push the 19 | commits with one `git push`, git preserves your commit info. 20 | 21 | i18n and documentation fixes, however large they are, may be directly 22 | committed without prior notification. 23 | 24 | ## Coding Style 25 | 26 | Spectacle follows the KDELibs coding style, with a few exceptions: 27 | 28 | 1. In class definitions, access modifiers are aligned along with 29 | member declarations, i.e., at one level right. E.g.: 30 | 31 | ```cpp 32 | class Hello : public QObject 33 | { 34 | Q_OBJECT 35 | 36 | public: 37 | 38 | void function(); 39 | } 40 | ``` 41 | 42 | The access modifier ordering is: public, signals, public slots, 43 | protected slots, protected, private slots, private. Member variables 44 | come at the end, after all member functions. This is not strictly 45 | enforced, but is a good rule to follow. 46 | 47 | 2. Member variables follow the format `mCamelCase`, and `not m_camelCase` 48 | which is more common throughout the rest of the KDE Applications. 49 | 50 | 3. Source files are mixed case, named the same as the class they 51 | contain. i.e., `SomeClass` will be defined in `SomeClass.cpp`, not 52 | `someclass.cpp`. 53 | 54 | ### Happy coding! 55 | -------------------------------------------------------------------------------- /src/Gui/CaptureModeButtonsColumn.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick 6 | import QtQuick.Layouts 7 | import QtQuick.Controls as QQC 8 | import org.kde.kirigami as Kirigami 9 | import org.kde.spectacle.private 10 | 11 | ColumnLayout { 12 | spacing: Kirigami.Units.mediumSpacing 13 | Repeater { 14 | model: CaptureModeModel 15 | delegate: QQC.DelayButton { 16 | id: button 17 | readonly property bool showCancel: Settings.captureMode === model.captureMode && SpectacleCore.captureTimeRemaining > 0 18 | Layout.fillWidth: true 19 | leftPadding: Kirigami.Units.mediumSpacing + QmlUtils.fontMetrics.descent 20 | rightPadding: Kirigami.Units.mediumSpacing + QmlUtils.fontMetrics.descent 21 | topPadding: Kirigami.Units.mediumSpacing 22 | bottomPadding: Kirigami.Units.mediumSpacing 23 | // Delay doesn't really matter since we set 24 | // progress directly and have no transition 25 | delay: 1 26 | transition: null 27 | progress: Settings.captureMode === model.captureMode ? 28 | SpectacleCore.captureProgress : 0 29 | icon.name: showCancel ? "dialog-cancel" : "" 30 | text: showCancel ? 31 | i18np("Cancel (%1 second)", "Cancel (%1 seconds)", 32 | Math.ceil(SpectacleCore.captureTimeRemaining / 1000)) 33 | : model.display 34 | QQC.ToolTip.text: model.shortcuts 35 | QQC.ToolTip.visible: (hovered || pressed) && model.shortcuts.length > 0 36 | QQC.ToolTip.delay: Kirigami.Units.toolTipDelay 37 | onClicked: if (showCancel) { 38 | SpectacleCore.cancelScreenshot() 39 | } else { 40 | Settings.captureMode = model.captureMode 41 | SpectacleCore.takeNewScreenshot() 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Platforms/PlatformNull.cpp: -------------------------------------------------------------------------------- 1 | /* This file is part of Spectacle, the KDE screenshot utility 2 | * SPDX-FileCopyrightText: 2019 Boudhayan Gupta 3 | 4 | * SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | #include "PlatformNull.h" 8 | 9 | #include 10 | #include 11 | 12 | /* -- Null Platform ---------------------------------------------------------------------------- */ 13 | 14 | ImagePlatformNull::ImagePlatformNull(QObject *parent) 15 | : ImagePlatform(parent) 16 | { 17 | } 18 | 19 | ImagePlatform::GrabModes ImagePlatformNull::supportedGrabModes() const 20 | { 21 | return {GrabMode::AllScreens | GrabMode::CurrentScreen | GrabMode::ActiveWindow | GrabMode::WindowUnderCursor | GrabMode::TransientWithParent 22 | | GrabMode::AllScreensScaled}; 23 | } 24 | 25 | ImagePlatform::ShutterModes ImagePlatformNull::supportedShutterModes() const 26 | { 27 | return {ShutterMode::Immediate | ShutterMode::OnClick}; 28 | } 29 | 30 | void ImagePlatformNull::doGrab(ShutterMode shutterMode, GrabMode grabMode, bool includePointer, bool includeDecorations, bool includeShadow) 31 | { 32 | Q_UNUSED(shutterMode) 33 | Q_UNUSED(grabMode) 34 | Q_UNUSED(includePointer) 35 | Q_UNUSED(includeDecorations) 36 | Q_UNUSED(includeShadow) 37 | Q_EMIT newScreenshotFailed(); 38 | } 39 | 40 | VideoPlatformNull::VideoPlatformNull(const QString &unavailableMessage, QObject *parent) 41 | : VideoPlatform(parent) 42 | , m_unavailableMessage(unavailableMessage) 43 | { 44 | } 45 | 46 | VideoPlatform::RecordingModes VideoPlatformNull::supportedRecordingModes() const 47 | { 48 | return {}; 49 | } 50 | 51 | VideoPlatform::Formats VideoPlatformNull::supportedFormats() const 52 | { 53 | return {}; 54 | } 55 | 56 | void VideoPlatformNull::startRecording(const QUrl &fileUrl, RecordingMode mode, const QVariantMap &options, bool withPointer) 57 | { 58 | Q_UNUSED(fileUrl) 59 | Q_UNUSED(mode) 60 | Q_UNUSED(options) 61 | Q_UNUSED(withPointer) 62 | Q_EMIT recordingFailed(m_unavailableMessage); 63 | } 64 | 65 | void VideoPlatformNull::finishRecording() 66 | { 67 | } 68 | 69 | #include "moc_PlatformNull.cpp" 70 | -------------------------------------------------------------------------------- /kconf_update/spectacle-24.02.0-rename_settings.cpp: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2023 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #include "ConfigUtils.h" 6 | #include 7 | #include 8 | 9 | using namespace Qt::StringLiterals; 10 | 11 | int main() 12 | { 13 | // We only need to read spectaclerc, so we use SimpleConfig. 14 | auto spectaclerc = KSharedConfig::openConfig("spectaclerc"_L1, KConfig::SimpleConfig); 15 | 16 | auto general = spectaclerc->group(QStringLiteral("General")); 17 | KeyMap generalOldNewMap{ 18 | // Using a name that doesn't look like a signal handler. 19 | {"onLaunchAction", "launchAction"}, 20 | // printKeyActionRunning looks like a bool 21 | {"printKeyActionRunning", "printKeyRunningAction"}, 22 | // shorten name and make it consistent with selectionRect 23 | {"rememberLastRectangularRegion", "rememberSelectionRect"}, 24 | }; 25 | replaceEntryKeys(general, generalOldNewMap); 26 | // Fix spelling 27 | replaceEntryValues(general, "launchAction", 28 | {{u"UseLastUsedCapturemode"_s, u"UseLastUsedCaptureMode"_s}}); 29 | // Shorten enum values 30 | replaceEntryValues(general, "rememberSelectionRect", 31 | {{u"UntilSpectacleIsClosed"_s, u"UntilClosed"_s}}); 32 | 33 | auto guiConfig = spectaclerc->group(QStringLiteral("GuiConfig")); 34 | KeyMap guiConfigOldNewMap{ 35 | // More in line with naming elsewhere. 36 | {"cropRegion", "selectionRect"}, 37 | // Using a name that doesn't look like a signal handler. 38 | {"onClickChecked", "captureOnClick"}, 39 | // Using a consistent spelling for color in code. 40 | {"useLightMaskColour", "useLightMaskColor"}, 41 | }; 42 | replaceEntryKeys(guiConfig, guiConfigOldNewMap); 43 | 44 | auto imageSave = spectaclerc->group(QStringLiteral("ImageSave")); 45 | replaceEntryKeys(imageSave, {{"imageFilenameFormat", "imageFilenameTemplate"}}); 46 | 47 | auto videoSave = spectaclerc->group(QStringLiteral("VideoSave")); 48 | replaceEntryKeys(videoSave, {{"videoFilenameFormat", "videoFilenameTemplate"}}); 49 | 50 | return spectaclerc->sync() ? 0 : 1; 51 | } 52 | -------------------------------------------------------------------------------- /src/Platforms/ImagePlatform.h: -------------------------------------------------------------------------------- 1 | /* This file is part of Spectacle, the KDE screenshot utility 2 | * SPDX-FileCopyrightText: 2019 Boudhayan Gupta 3 | 4 | * SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | #pragma once 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | class ImagePlatform : public QObject 15 | { 16 | Q_OBJECT 17 | QML_ELEMENT 18 | QML_UNCREATABLE("Created by SpectacleCore") 19 | 20 | Q_PROPERTY(GrabModes supportedGrabModes READ supportedGrabModes NOTIFY supportedGrabModesChanged) 21 | // Currently, supportedShutterModes never changes. 22 | // Be sure to add a changed signal if it is ever able to change. 23 | Q_PROPERTY(ShutterModes supportedShutterModes READ supportedShutterModes CONSTANT) 24 | 25 | public: 26 | enum GrabMode { 27 | NoGrabModes = 0b0000000, 28 | AllScreens = 0b0000001, 29 | CurrentScreen = 0b0000010, 30 | ActiveWindow = 0b0000100, 31 | WindowUnderCursor = 0b0001000, 32 | TransientWithParent = 0b0010000, 33 | AllScreensScaled = 0b0100000, 34 | PerScreenImageNative = 0b1000000, 35 | }; 36 | Q_DECLARE_FLAGS(GrabModes, GrabMode) 37 | Q_FLAG(GrabModes) 38 | 39 | enum ShutterMode { Immediate = 0x01, OnClick = 0x02 }; 40 | Q_DECLARE_FLAGS(ShutterModes, ShutterMode) 41 | Q_FLAG(ShutterModes) 42 | 43 | explicit ImagePlatform(QObject *parent = nullptr); 44 | ~ImagePlatform() override = default; 45 | 46 | virtual GrabModes supportedGrabModes() const = 0; 47 | virtual ShutterModes supportedShutterModes() const = 0; 48 | 49 | public Q_SLOTS: 50 | virtual void 51 | doGrab(ImagePlatform::ShutterMode shutterMode, ImagePlatform::GrabMode grabMode, bool includePointer, bool includeDecorations, bool includeShadow) = 0; 52 | 53 | Q_SIGNALS: 54 | void supportedGrabModesChanged(); 55 | 56 | void newScreenshotTaken(const QImage &image = {}); 57 | void newCroppableScreenshotTaken(const QImage &image); 58 | 59 | void newScreenshotFailed(const QString &message = {}); 60 | void newScreenshotCanceled(); 61 | }; 62 | 63 | Q_DECLARE_OPERATORS_FOR_FLAGS(ImagePlatform::GrabModes) 64 | Q_DECLARE_OPERATORS_FOR_FLAGS(ImagePlatform::ShutterModes) 65 | -------------------------------------------------------------------------------- /src/CaptureModeModel.h: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #pragma once 6 | 7 | #include "Platforms/ImagePlatform.h" 8 | 9 | #include 10 | #include 11 | 12 | /** 13 | * This is a model containing the current supported capture modes and their labels and shortcuts. 14 | */ 15 | class CaptureModeModel : public QAbstractListModel 16 | { 17 | Q_OBJECT 18 | QML_ELEMENT 19 | QML_SINGLETON 20 | Q_PROPERTY(int count READ rowCount NOTIFY countChanged FINAL) 21 | 22 | public: 23 | CaptureModeModel(QObject *parent = nullptr); 24 | 25 | static CaptureModeModel *instance(); 26 | 27 | static CaptureModeModel *create(QQmlEngine *engine, QJSEngine *) 28 | { 29 | auto inst = instance(); 30 | Q_ASSERT(inst); 31 | Q_ASSERT(inst->thread() == engine->thread()); 32 | QJSEngine::setObjectOwnership(inst, QJSEngine::CppOwnership); 33 | return inst; 34 | } 35 | 36 | enum CaptureMode { 37 | RectangularRegion, 38 | AllScreens, 39 | // TODO: find a more user configuration friendly way to scale source images 40 | AllScreensScaled, 41 | CurrentScreen, 42 | ActiveWindow, 43 | WindowUnderCursor, 44 | FullScreen, 45 | }; 46 | Q_ENUM(CaptureMode) 47 | 48 | enum { 49 | CaptureModeRole = Qt::UserRole + 1, 50 | ShortcutsRole = Qt::UserRole + 2, 51 | }; 52 | 53 | QHash roleNames() const override; 54 | QVariant data(const QModelIndex &index, int role) const override; 55 | int rowCount(const QModelIndex &parent = QModelIndex()) const override; 56 | 57 | int indexOfCaptureMode(CaptureMode captureMode) const; 58 | 59 | void setGrabModes(ImagePlatform::GrabModes modes); 60 | 61 | static QString captureModeLabel(CaptureMode mode); 62 | 63 | Q_SIGNALS: 64 | void captureModesChanged(); 65 | void countChanged(); 66 | 67 | private: 68 | struct Item { 69 | CaptureModeModel::CaptureMode captureMode; 70 | QString label; 71 | QString shortcuts = {}; // default value in case there's nothing 72 | }; 73 | 74 | QList m_data; 75 | QHash m_roleNames; 76 | ImagePlatform::GrabModes m_grabModes; 77 | }; 78 | -------------------------------------------------------------------------------- /kconf_update/spectacle.upd: -------------------------------------------------------------------------------- 1 | # This is meant to be a unified kconf_update file. When you need to update the 2 | # config file, add a new section in this file. 3 | # 4 | # Each section should have a comment saying what it's for just above it. 5 | # 6 | # Each section ID should have a version prefix indicating the release version 7 | # for which it was added, followed by a topic string using underscores for 8 | # spaces, followed by an increment suffix. The latter should only be used if for 9 | # some reason you need to do more than one config update for the same release 10 | # version with the same topic. These parts should be connected by hyphens. 11 | # Examples: `24.02.0-example_update`, `24.02.0-example_update-2`. 12 | # 13 | # If you are using `ScriptArguments=`, always put it before `Script=`. 14 | # 15 | # Scripts should be named after the basename of this `.upd` file, followed by 16 | # the name of their associated section, connected by a hyphen. 17 | # Example: `spectacle-24.02.0-example_update.py`. 18 | # This way the `spectacle.upd` file and the scripts are sorted next to each 19 | # other in `/usr/share/kconf_update` and it's easy to identify associated 20 | # sections added to the `update_info` entry of `spectaclerc`. 21 | 22 | # KDE Frameworks/kconf_update version. 23 | # This only needs to be changed when the version of kconf_update changes. 24 | Version=6 25 | 26 | # This is an example section. 27 | # Id=24.02.0-example_update 28 | # Script=spectacle-24.02.0-example_update.py,python3 29 | 30 | # Remove old video settings. Separate save related image and video settings. 31 | Id=24.02.0-video_format 32 | Script=spectacle-24.02.0-video_format 33 | 34 | # Keep old default image and video save locations for users with existing configs 35 | Id=24.02.0-keep_old_save_location 36 | Script=spectacle-24.02.0-keep_old_save_location 37 | 38 | # Rename some settings 39 | Id=24.02.0-rename_settings 40 | Script=spectacle-24.02.0-rename_settings 41 | 42 | # Keep old default image and video filename templates for users with existing configs 43 | Id=24.02.0-keep_old_filename_templates 44 | Script=spectacle-24.02.0-keep_old_filename_templates 45 | 46 | # Change filename placeholders to new format 47 | # We renamed the Id to 24.05.2-change_placeholder_format 48 | # in order to trigger the update again. 49 | Id=24.05.2-change_placeholder_format 50 | Script=spectacle-24.02.0-change_placeholder_format 51 | -------------------------------------------------------------------------------- /src/Gui/Magnifier.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022 Noah Davis 2 | * SPDX-FileCopyrightText: 2022 Marco Martin 3 | * SPDX-License-Identifier: LGPL-2.0-or-later 4 | */ 5 | 6 | import QtQuick 7 | import QtQuick.Window 8 | import QtQuick.Layouts 9 | import org.kde.kirigami as Kirigami 10 | import org.kde.spectacle.private 11 | import org.kde.kquickimageeditor 12 | 13 | ShaderEffectSource { 14 | id: root 15 | required property AnnotationViewport viewport 16 | required property point targetPoint 17 | property int factor: 3 18 | 19 | implicitWidth: { 20 | const w = Kirigami.Units.gridUnit * 10 21 | return w - w % factor - factor 22 | } 23 | implicitHeight: implicitWidth 24 | sourceItem: viewport 25 | sourceRect: Qt.rect((targetPoint.x - viewport.viewportRect.x) - implicitWidth / (factor * 2), 26 | (targetPoint.y - viewport.viewportRect.y) - implicitHeight / (factor * 2), 27 | implicitWidth / factor, implicitHeight / factor) 28 | smooth: false 29 | 30 | Item { 31 | id: center 32 | x: contextWindow.dprRound((parent.implicitWidth - width) / 2) 33 | y: contextWindow.dprRound((parent.implicitHeight - height) / 2) 34 | width: root.factor * 3 35 | height: root.factor * 3 36 | } 37 | 38 | Rectangle { // top 39 | anchors.top: parent.top 40 | anchors.bottom: center.top 41 | color: Kirigami.Theme.focusColor 42 | x: contextWindow.dprRound((parent.implicitWidth - width) / 2) 43 | width: root.factor 44 | } 45 | Rectangle { // bottom 46 | anchors.bottom: parent.bottom 47 | anchors.top: center.bottom 48 | color: Kirigami.Theme.focusColor 49 | x: contextWindow.dprRound((parent.implicitWidth - width) / 2) 50 | width: root.factor 51 | } 52 | Rectangle { // left 53 | anchors.left: parent.left 54 | anchors.right: center.left 55 | color: Kirigami.Theme.focusColor 56 | y: contextWindow.dprRound((parent.implicitHeight - height) / 2) 57 | height: root.factor 58 | } 59 | Rectangle { // right 60 | anchors.right: parent.right 61 | anchors.left: center.right 62 | color: Kirigami.Theme.focusColor 63 | y: contextWindow.dprRound((parent.implicitHeight - height) / 2) 64 | height: root.factor 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Gui/SpectacleMenu.cpp: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #include "SpectacleMenu.h" 6 | #include "WidgetWindowUtils.h" 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | SpectacleMenu::SpectacleMenu(const QString &title, QWidget *parent) 13 | : QMenu(title, parent) 14 | { 15 | setAttribute(Qt::WA_TranslucentBackground); 16 | } 17 | 18 | SpectacleMenu::SpectacleMenu(QWidget *parent) 19 | : QMenu(parent) 20 | { 21 | setAttribute(Qt::WA_TranslucentBackground); 22 | } 23 | 24 | void SpectacleMenu::setVisible(bool visible) 25 | { 26 | bool oldVisible = isVisible(); 27 | if (oldVisible == visible) { 28 | return; 29 | } 30 | // Workaround for a bug where Qt Quick buttons always open the menu even when the menu is already open 31 | if (visible) { 32 | QMenu::setVisible(true); 33 | } else { 34 | QTimer::singleShot(200, this, [this] { 35 | QMenu::setVisible(false); 36 | }); 37 | } 38 | } 39 | 40 | void SpectacleMenu::popup(QQuickItem *item) 41 | { 42 | if (!item || !item->window()) { 43 | return; 44 | } 45 | auto itemWindow = item->window(); 46 | auto point = item->mapToGlobal({0, item->height()}); 47 | auto screenRect = itemWindow->screen()->geometry(); 48 | auto sizeHint = this->sizeHint(); 49 | if (point.y() + sizeHint.height() > screenRect.bottom()) { 50 | point.setY(point.y() - item->height() - sizeHint.height()); 51 | } 52 | if (point.x() + sizeHint.width() > screenRect.right()) { 53 | point.setX(point.x() - sizeHint.width() + item->width()); 54 | } 55 | setWidgetTransientParent(this, itemWindow); 56 | // Workaround same as plasma to have click anywhereto close the menu 57 | QTimer::singleShot(0, this, [this, itemWindow, point]() { 58 | if (itemWindow->mouseGrabberItem()) { 59 | itemWindow->mouseGrabberItem()->ungrabMouse(); 60 | } 61 | QMenu::popup(point.toPoint()); 62 | }); 63 | } 64 | 65 | void SpectacleMenu::showEvent(QShowEvent *event) 66 | { 67 | QMenu::showEvent(event); 68 | Q_EMIT visibleChanged(); 69 | } 70 | 71 | void SpectacleMenu::hideEvent(QHideEvent *event) 72 | { 73 | QMenu::hideEvent(event); 74 | Q_EMIT visibleChanged(); 75 | } 76 | 77 | #include "moc_SpectacleMenu.cpp" 78 | -------------------------------------------------------------------------------- /src/Gui/InlineMessageModel.h: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #pragma once 6 | 7 | #include 8 | #include 9 | 10 | /** 11 | * This is a model containing the current supported capture modes and their labels and shortcuts. 12 | */ 13 | class InlineMessageModel : public QAbstractListModel 14 | { 15 | Q_OBJECT 16 | QML_ELEMENT 17 | QML_SINGLETON 18 | Q_PROPERTY(int count READ rowCount NOTIFY countChanged FINAL) 19 | 20 | public: 21 | static InlineMessageModel *instance(); 22 | 23 | static InlineMessageModel *create(QQmlEngine *engine, QJSEngine *) 24 | { 25 | auto inst = instance(); 26 | Q_ASSERT(inst); 27 | Q_ASSERT(inst->thread() == engine->thread()); 28 | QJSEngine::setObjectOwnership(inst, QJSEngine::CppOwnership); 29 | return inst; 30 | } 31 | 32 | enum CustomRoles { 33 | TypeRole = Qt::UserRole + 1, 34 | DataRole = Qt::UserRole + 2, 35 | }; 36 | Q_ENUM(CustomRoles) 37 | 38 | enum InlineMessageType { 39 | // Warnings and errors don't replace others of the same enum value. 40 | Error, 41 | Warning, 42 | // Informational types replace others of the same enum value. 43 | InformationalType, 44 | Copied = InformationalType, 45 | Saved = InformationalType + 1, 46 | Shared = InformationalType + 2, 47 | Scanned = InformationalType + 3, 48 | }; 49 | Q_ENUM(InlineMessageType) 50 | 51 | QHash roleNames() const override; 52 | 53 | QVariant data(const QModelIndex &index, int role) const override; 54 | 55 | int rowCount(const QModelIndex &parent = {}) const override; 56 | 57 | Q_INVOKABLE void push(InlineMessageType type, const QString &text, const QVariant &data = {}); 58 | Q_INVOKABLE void pop(int row = -1); 59 | Q_INVOKABLE void clear(); 60 | 61 | Q_INVOKABLE void copyToClipboard(const QVariant &content); 62 | Q_INVOKABLE void openContainingFolder(const QUrl &url); 63 | 64 | Q_SIGNALS: 65 | void countChanged(); 66 | 67 | private: 68 | InlineMessageModel(QObject *parent = nullptr); 69 | 70 | struct Item { 71 | InlineMessageType type; 72 | QString text; 73 | QVariant data; 74 | }; 75 | 76 | QList m_data; 77 | QHash m_roleNames; 78 | }; 79 | -------------------------------------------------------------------------------- /src/Gui/SettingsDialog/GeneralOptionsPage.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Jhair Paris 3 | * SPDX-FileCopyrightText: 2019 David Redondo 4 | * SPDX-FileCopyrightText: 2015 Boudhayan Gupta 5 | * 6 | * SPDX-License-Identifier: LGPL-2.0-or-later 7 | */ 8 | 9 | #include "GeneralOptionsPage.h" 10 | 11 | #include "OcrLanguageSelector.h" 12 | #include "OcrManager.h" 13 | #include "settings.h" 14 | #include "ui_GeneralOptions.h" 15 | 16 | #include 17 | #include 18 | 19 | #include 20 | 21 | using namespace Qt::Literals::StringLiterals; 22 | 23 | GeneralOptionsPage::GeneralOptionsPage(QWidget *parent) 24 | : QWidget(parent) 25 | , m_ui(new Ui_GeneralOptions) 26 | , m_ocrLanguageSelector(new OcrLanguageSelector(this)) 27 | { 28 | m_ui->setupUi(this); 29 | 30 | m_ui->ocrInfoIcon->setPixmap(QIcon::fromTheme(QStringLiteral("help-hint")).pixmap(16, 16)); 31 | m_ui->ocrInfoIcon->setCursor(Qt::WhatsThisCursor); 32 | 33 | m_ui->runningTitle->setLevel(2); 34 | m_ui->regionTitle->setLevel(2); 35 | m_ui->ocrTitle->setLevel(2); 36 | 37 | m_ui->ocrLanguageScrollArea->setWidget(m_ocrLanguageSelector); 38 | m_ui->ocrLanguageScrollArea->setWidgetResizable(true); 39 | 40 | connect(m_ocrLanguageSelector, &OcrLanguageSelector::selectedLanguagesChanged, this, &GeneralOptionsPage::ocrLanguageChanged); 41 | 42 | refreshOcrLanguageSettings(); 43 | 44 | //On Wayland we can't programmatically raise and focus the window so we have to hide the option 45 | if (KWindowSystem::isPlatformWayland() || qstrcmp(qgetenv("XDG_SESSION_TYPE").constData(), "wayland") == 0) { 46 | m_ui->kcfg_printKeyRunningAction->removeItem(2); 47 | } 48 | } 49 | 50 | GeneralOptionsPage::~GeneralOptionsPage() = default; 51 | 52 | void GeneralOptionsPage::refreshOcrLanguageSettings() 53 | { 54 | OcrManager *ocrManager = OcrManager::instance(); 55 | 56 | if (!ocrManager->isAvailable()) { 57 | m_ui->ocrLanguageLabel->setVisible(false); 58 | m_ui->ocrLanguageScrollArea->setVisible(false); 59 | m_ui->ocrUnavailableWidget->setVisible(true); 60 | } else { 61 | m_ui->ocrLanguageLabel->setVisible(true); 62 | m_ui->ocrLanguageScrollArea->setVisible(true); 63 | m_ui->ocrUnavailableWidget->setVisible(false); 64 | 65 | m_ocrLanguageSelector->refresh(); 66 | } 67 | } 68 | 69 | #include "moc_GeneralOptionsPage.cpp" 70 | -------------------------------------------------------------------------------- /kconf_update/spectacle-24.02.0-change_placeholder_format.cpp: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2023 Noah Davis 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #include "ConfigUtils.h" 6 | #include 7 | #include 8 | #include 9 | 10 | using namespace Qt::StringLiterals; 11 | 12 | const ValueMap oldNewMap{ 13 | {u"%Y"_s, u""_s}, 14 | {u"%y"_s, u""_s}, 15 | {u"%M"_s, u""_s}, 16 | {u"%n"_s, u""_s}, 17 | {u"%N"_s, u""_s}, 18 | {u"%D"_s, u"
"_s}, 19 | {u"%H"_s, u""_s}, 20 | {u"%m"_s, u""_s}, 21 | {u"%S"_s, u""_s}, 22 | {u"%t"_s, u""_s}, 23 | {u"%T"_s, u""_s}, 24 | }; 25 | 26 | inline QString changedFormat(QString filenameTemplate) 27 | { 28 | for (auto it = oldNewMap.cbegin(); it != oldNewMap.cend(); ++it) { 29 | if (filenameTemplate.contains(it.key()) && !filenameTemplate.contains(it.value())) { 30 | filenameTemplate.replace(it.key(), it.value()); 31 | } 32 | } 33 | 34 | QRegularExpression sequenceRE(u"%(\\d*)d"_s); 35 | auto it = sequenceRE.globalMatch(filenameTemplate); 36 | while (it.hasNext()) { 37 | auto match = it.next(); 38 | int padding = 0; 39 | if (!match.captured(1).isEmpty()) { 40 | padding = match.captured(1).toInt(); 41 | } 42 | auto newValue = u"<%1>"_s.arg(u"#"_s, padding, u'#'); 43 | filenameTemplate.replace(match.captured(), newValue); 44 | } 45 | 46 | return filenameTemplate; 47 | } 48 | 49 | int main() 50 | { 51 | const auto fileName = u"spectaclerc"_s; 52 | // We only need to read spectaclerc, so we use SimpleConfig. 53 | auto spectaclerc = KSharedConfig::openConfig(fileName, KConfig::SimpleConfig); 54 | 55 | auto imageSaveGroup = spectaclerc->group(QStringLiteral("ImageSave")); 56 | if (!isEntryDefault(imageSaveGroup, "imageFilenameTemplate")) { 57 | auto value = imageSaveGroup.readEntry("imageFilenameTemplate"); 58 | imageSaveGroup.writeEntry("imageFilenameTemplate", changedFormat(value)); 59 | } 60 | 61 | auto videoSaveGroup = spectaclerc->group(QStringLiteral("VideoSave")); 62 | if (!isEntryDefault(videoSaveGroup, "videoFilenameTemplate")) { 63 | auto value = videoSaveGroup.readEntry("videoFilenameTemplate"); 64 | videoSaveGroup.writeEntry("videoFilenameTemplate", changedFormat(value)); 65 | } 66 | 67 | return spectaclerc->sync() ? 0 : 1; 68 | } 69 | -------------------------------------------------------------------------------- /src/Gui/Outline.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022 Noah Davis <noahadvs@gmail.com> 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick 6 | import QtQuick.Shapes 7 | import org.kde.kirigami as Kirigami 8 | import org.kde.spectacle.private 9 | 10 | Shape { 11 | id: root 12 | property alias strokeWidth: shapePath.strokeWidth 13 | // The stroke color beneath the dash 14 | property alias strokeColor: shapePath.strokeColor 15 | property alias strokeStyle: shapePath.strokeStyle 16 | property alias capStyle: shapePath.capStyle 17 | property alias joinStyle: shapePath.joinStyle 18 | property alias svgPath: pathSvg.path 19 | property alias pathScale: shapePath.scale 20 | property alias pathHints: shapePath.pathHints 21 | 22 | // Get a rectangular SVG path 23 | function rectanglePath(x, y, w, h) { 24 | // absolute start at top-left, 25 | // relative line to top-right, 26 | // relative line to bottom-right 27 | // relative line to bottom-left 28 | // close path (automatic line to top-left) 29 | return `M ${x},${y} 30 | l ${w},0 31 | l 0,${h} 32 | l ${-w},0 33 | z` 34 | } 35 | 36 | // Get a matrix4x4 that moves the stroke outside the bounds of the path 37 | function outerStrokeScaleValue(originalValue, strokeWidth = root.strokeWidth) { 38 | return QmlUtils.ratio(originalValue + strokeWidth * 2, originalValue + strokeWidth) 39 | } 40 | 41 | // Get a matrix4x4 that moves the stroke outside the bounds of the path 42 | function outerStrokeTranslateValue(originalValue, scale, strokeWidth = root.strokeWidth) { 43 | return QmlUtils.unTranslateScale(originalValue, scale) - strokeWidth / 2 44 | } 45 | 46 | preferredRendererType: Shape.CurveRenderer 47 | 48 | ShapePath { 49 | id: shapePath 50 | fillColor: "transparent" 51 | // ensure outline is always thick enough to be visible, but grows with zoom 52 | strokeWidth: dprRound(1) 53 | strokeColor: palette.highlight 54 | // Solid line because it's easier to do the alternating color effect this way. 55 | strokeStyle: ShapePath.SolidLine 56 | joinStyle: ShapePath.MiterJoin 57 | PathSvg { 58 | id: pathSvg 59 | path: rectanglePath(strokeWidth / 2, strokeWidth / 2, 60 | width - strokeWidth, height - strokeWidth) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Gui/SettingsDialog/VideoSaveOptions.ui: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <ui version="4.0"> 3 | <class>VideoSaveOptions</class> 4 | <widget class="QWidget" name="VideoSaveOptions"> 5 | <layout class="QFormLayout" name="formLayout"> 6 | <item row="1" column="0"> 7 | <widget class="QLabel" name="saveLocationLabel"> 8 | <property name="text"> 9 | <string>Save &Location:</string> 10 | </property> 11 | <property name="buddy"> 12 | <cstring>kcfg_videoSaveLocation</cstring> 13 | </property> 14 | </widget> 15 | </item> 16 | <item row="1" column="1"> 17 | <widget class="KUrlRequester" name="kcfg_videoSaveLocation"> 18 | <property name="mode"> 19 | <set>KFile::Directory|KFile::LocalOnly</set> 20 | </property> 21 | </widget> 22 | </item> 23 | <item row="2" column="0"> 24 | <widget class="QLabel" name="filenameLabel"> 25 | <property name="text"> 26 | <string>Filename:</string> 27 | </property> 28 | <property name="buddy"> 29 | <cstring>kcfg_videoFilenameTemplate</cstring> 30 | </property> 31 | </widget> 32 | </item> 33 | <item row="2" column="1"> 34 | <layout class="QHBoxLayout" name="saveLayout"> 35 | <item> 36 | <widget class="QLineEdit" name="kcfg_videoFilenameTemplate"> 37 | <property name="placeholderText"> 38 | <string notr="true">%d</string> 39 | </property> 40 | </widget> 41 | </item> 42 | </layout> 43 | </item> 44 | <item row="3" column="0"> 45 | <widget class="QLabel" name="previewLabel"> 46 | <property name="text"> 47 | <string comment="Preview of the user configured filename">Preview:</string> 48 | </property> 49 | </widget> 50 | </item> 51 | <item row="3" column="1"> 52 | <widget class="QLabel" name="preview"> 53 | <property name="text"> 54 | <string/> 55 | </property> 56 | </widget> 57 | </item> 58 | <item row="5" column="1"> 59 | <widget class="QLabel" name="captureInstructionLabel"> 60 | <property name="text"> 61 | <string/> 62 | </property> 63 | <property name="textFormat"> 64 | <enum>Qt::RichText</enum> 65 | </property> 66 | <property name="wordWrap"> 67 | <bool>true</bool> 68 | </property> 69 | </widget> 70 | </item> 71 | </layout> 72 | </widget> 73 | <customwidgets> 74 | <customwidget> 75 | <class>KUrlRequester</class> 76 | <extends>QWidget</extends> 77 | <header>kurlrequester.h</header> 78 | </customwidget> 79 | </customwidgets> 80 | <resources/> 81 | <connections/> 82 | </ui> 83 | -------------------------------------------------------------------------------- /src/Gui/RecordingModeMenu.cpp: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2025 Noah Davis <noahadvs@gmail.com> 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #include "RecordingModeMenu.h" 6 | #include "RecordingModeModel.h" 7 | #include "SpectacleCore.h" 8 | #include "ShortcutActions.h" 9 | #include <KGlobalAccel> 10 | 11 | using namespace Qt::StringLiterals; 12 | 13 | static QPointer<RecordingModeMenu> s_instance = nullptr; 14 | 15 | RecordingModeMenu::RecordingModeMenu(QWidget *parent) 16 | : SpectacleMenu(i18nc("@title:menu", "Recording Modes"), parent) 17 | { 18 | auto addModes = [this] { 19 | clear(); 20 | auto model = RecordingModeModel::instance(); 21 | for (auto idx = model->index(0); idx.isValid(); idx = idx.siblingAtRow(idx.row() + 1)) { 22 | const auto action = addAction(idx.data(Qt::DisplayRole).toString()); 23 | const auto mode = idx.data(RecordingModeModel::RecordingModeRole).value<VideoPlatform::RecordingMode>(); 24 | QAction *globalAction = nullptr; 25 | auto globalShortcuts = [](QAction *globalAction) { 26 | if (!globalAction) { 27 | return QList<QKeySequence>{}; 28 | } 29 | auto component = ShortcutActions::self()->componentName(); 30 | auto id = globalAction->objectName(); 31 | return KGlobalAccel::self()->globalShortcut(component, id); 32 | }; 33 | switch (mode) { 34 | case VideoPlatform::Region: 35 | globalAction = ShortcutActions::self()->recordRegionAction(); 36 | break; 37 | case VideoPlatform::Screen: 38 | globalAction = ShortcutActions::self()->recordScreenAction(); 39 | break; 40 | case VideoPlatform::Window: 41 | globalAction = ShortcutActions::self()->recordWindowAction(); 42 | break; 43 | default: 44 | break; 45 | } 46 | action->setShortcuts(globalShortcuts(globalAction)); 47 | auto onTriggered = [mode] { 48 | SpectacleCore::instance()->startRecording(mode); 49 | }; 50 | connect(action, &QAction::triggered, action, onTriggered); 51 | } 52 | }; 53 | addModes(); 54 | connect(RecordingModeModel::instance(), &RecordingModeModel::recordingModesChanged, this, addModes); 55 | } 56 | 57 | RecordingModeMenu *RecordingModeMenu::instance() 58 | { 59 | if (!s_instance) { 60 | s_instance = new RecordingModeMenu; 61 | } 62 | return s_instance; 63 | } 64 | 65 | #include "moc_RecordingModeMenu.cpp" 66 | -------------------------------------------------------------------------------- /src/PlasmaVersion.cpp: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2023 Noah Davis <noahadvs@gmail.com> 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #include "PlasmaVersion.h" 6 | 7 | #include <QDBusConnection> 8 | #include <QDBusConnectionInterface> 9 | #include <QDBusMessage> 10 | #include <QDBusVariant> 11 | #include <QDebug> 12 | 13 | using namespace Qt::StringLiterals; 14 | 15 | static quint32 s_plasmaVersion = 0; 16 | static const auto s_plasmashellService = u"org.kde.plasmashell"_s; 17 | 18 | quint32 PlasmaVersion::get() 19 | { 20 | if (!QDBusConnection::sessionBus().interface()->isServiceRegistered(s_plasmashellService)) { 21 | s_plasmaVersion = 0; 22 | } else if (s_plasmaVersion == 0) { 23 | auto message = QDBusMessage::createMethodCall(s_plasmashellService, 24 | u"/MainApplication"_s, 25 | u"org.freedesktop.DBus.Properties"_s, 26 | u"Get"_s); 27 | 28 | message.setArguments({u"org.qtproject.Qt.QCoreApplication"_s, u"applicationVersion"_s}); 29 | 30 | const auto resultMessage = QDBusConnection::sessionBus().call(message); 31 | if (resultMessage.type() != QDBusMessage::ReplyMessage) { 32 | qWarning() << "Error querying plasma version" << resultMessage.errorName() << resultMessage.errorMessage(); 33 | return s_plasmaVersion; 34 | } 35 | QDBusVariant val = resultMessage.arguments().at(0).value<QDBusVariant>(); 36 | 37 | const QString rawVersion = val.variant().value<QString>(); 38 | const QList<QStringView> splitted = QStringView(rawVersion).split(u'.'); 39 | if (splitted.size() != 3) { 40 | qWarning() << "error parsing plasma version"; 41 | return s_plasmaVersion; 42 | } 43 | bool ok; 44 | auto major = splitted[0].toShort(&ok); 45 | if (!ok) { 46 | qWarning() << "error parsing plasma major version"; 47 | return s_plasmaVersion; 48 | } 49 | auto minor = splitted[1].toShort(&ok); 50 | if (!ok) { 51 | qWarning() << "error parsing plasma minor version"; 52 | return s_plasmaVersion; 53 | } 54 | auto patch = splitted[2].toShort(&ok); 55 | if (!ok) { 56 | qWarning() << "error parsing plasma patch version"; 57 | return s_plasmaVersion; 58 | } 59 | s_plasmaVersion = check(major, minor, patch); 60 | } 61 | return s_plasmaVersion; 62 | } 63 | 64 | quint32 PlasmaVersion::check(quint8 major, quint8 minor, quint8 patch) 65 | { 66 | return (major << 16) | (minor << 8) | patch; 67 | } 68 | -------------------------------------------------------------------------------- /kconf_update/ConfigUtils.h: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2023 Noah Davis <noahadvs@gmail.com> 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #pragma once 6 | 7 | #include <KConfigGroup> 8 | #include <QDateTime> 9 | #include <QFileInfo> 10 | #include <QStandardPaths> 11 | 12 | // automatically use this when including this header 13 | using namespace Qt::StringLiterals; 14 | using KeyMap = QMap<const char *, const char *>; 15 | using ValueMap = QMap<QString, QString>; 16 | 17 | inline void replaceEntryKeys(KConfigGroup &group, const KeyMap &oldNewMap) 18 | { 19 | if (!group.exists()) { 20 | return; 21 | } 22 | for (auto it = oldNewMap.cbegin(); it != oldNewMap.cend(); ++it) { 23 | if (!group.hasKey(it.key())) { 24 | continue; 25 | } 26 | // Only write if new key is not empty 27 | if (!QByteArrayLiteral(it.value()).isEmpty()) { 28 | group.writeEntry(it.value(), group.readEntry(it.key())); 29 | } 30 | group.deleteEntry(it.key()); 31 | } 32 | }; 33 | 34 | inline void replaceEntryValues(KConfigGroup &group, const char *key, 35 | const ValueMap &oldNewMap) 36 | { 37 | if (!group.exists() || !group.hasKey(key)) { 38 | return; 39 | } 40 | for (auto it = oldNewMap.cbegin(); it != oldNewMap.cend(); ++it) { 41 | if (group.readEntry(key) != it.key()) { 42 | continue; 43 | } 44 | // Only write if new value is not empty 45 | if (!it.value().isEmpty()) { 46 | group.writeEntry(key, it.value()); 47 | } else { 48 | // Delete if new value is empty because it'll be removed anyway. 49 | group.deleteEntry(key); 50 | } 51 | } 52 | }; 53 | 54 | inline bool isFileOlderThanDateTime(const QString &fileName, const QString &isoDateTime = {}) 55 | { 56 | const auto path = QStandardPaths::locate(QStandardPaths::GenericConfigLocation, fileName); 57 | // false if there is no existing user config. 58 | if (path.isEmpty() || !path.startsWith(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation))) { 59 | return false; 60 | } 61 | 62 | // true if we aren't doing a datetime check 63 | if (isoDateTime.isEmpty()) { 64 | return true; 65 | } 66 | 67 | // false if the existing config is newer than the threshold datetime. 68 | QFileInfo fileInfo(path); 69 | auto configDateTime = fileInfo.birthTime(); 70 | auto thresholdDateTime = QDateTime::fromString(isoDateTime, Qt::ISODate); 71 | if (!configDateTime.isValid() || !thresholdDateTime.isValid() 72 | || configDateTime > thresholdDateTime) { 73 | return false; 74 | } 75 | 76 | return true; 77 | } 78 | 79 | inline bool isEntryDefault(KConfigGroup &group, const char *key) 80 | { 81 | return !group.exists() || group.readEntry(key).isEmpty(); 82 | } 83 | -------------------------------------------------------------------------------- /src/Gui/HelpMenu.cpp: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022 Noah Davis <noahadvs@gmail.com> 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #include "HelpMenu.h" 6 | #include "WidgetWindowUtils.h" 7 | 8 | #include <KAboutData> 9 | #include <KLocalizedString> 10 | 11 | #include <QApplication> 12 | #include <QDialog> 13 | #include <QWindow> 14 | 15 | #include <cstring> 16 | 17 | using namespace Qt::StringLiterals; 18 | 19 | class HelpMenuSingleton 20 | { 21 | public: 22 | HelpMenu self; 23 | }; 24 | 25 | Q_GLOBAL_STATIC(HelpMenuSingleton, privateHelpMenuSelf) 26 | 27 | static QObject *findWidgetOfType(const char *className) 28 | { 29 | if (strlen(className) == 0) { 30 | return nullptr; 31 | } 32 | const auto widgets = qApp->allWidgets(); 33 | for (const auto w : widgets) { 34 | if (w->inherits(className)) { 35 | return w; 36 | } 37 | } 38 | return nullptr; 39 | } 40 | 41 | HelpMenu::HelpMenu(QWidget* parent) 42 | : SpectacleMenu(parent) 43 | , kHelpMenu(new KHelpMenu(parent, KAboutData::applicationData(), true)) 44 | { 45 | setTitle(i18nc("@title:menu", "Help")); 46 | setIcon(QIcon::fromTheme(u"help-contents"_s)); 47 | addActions(kHelpMenu->menu()->actions()); 48 | connect(this, &QMenu::triggered, this, &HelpMenu::onTriggered); 49 | } 50 | 51 | HelpMenu *HelpMenu::instance() 52 | { 53 | return &privateHelpMenuSelf->self; 54 | } 55 | 56 | void HelpMenu::showAppHelp() 57 | { 58 | kHelpMenu->appHelpActivated(); 59 | } 60 | 61 | void HelpMenu::onTriggered(QAction *action) 62 | { 63 | auto transientParent = getWidgetTransientParent(this); 64 | if (!transientParent || !transientParent->isVisible() || action == kHelpMenu->action(KHelpMenu::menuWhatsThis)) { 65 | return; 66 | } 67 | 68 | QDialog *dialog = nullptr; 69 | // KHelpMenu creates these dialogs and sets the parent of KHelpMenu as the parent of the dialogs. 70 | // KHelpMenu doesn't expose pointers to the dialogs, 71 | // so we have to search for them in the parent. 72 | // 2 of the dialogs we need to find are private types. 73 | if (action == kHelpMenu->action(KHelpMenu::menuReportBug)) { 74 | dialog = qobject_cast<QDialog *>(findWidgetOfType("KBugReport")); 75 | } else if (action == kHelpMenu->action(KHelpMenu::menuSwitchLanguage)) { 76 | dialog = qobject_cast<QDialog *>(findWidgetOfType("KDEPrivate::KSwitchLanguageDialog")); 77 | } else if (action == kHelpMenu->action(KHelpMenu::menuAboutApp)) { 78 | dialog = qobject_cast<QDialog *>(findWidgetOfType("KAboutApplicationDialog")); 79 | } else if (action == kHelpMenu->action(KHelpMenu::menuAboutKDE)) { 80 | dialog = qobject_cast<QDialog *>(findWidgetOfType("KDEPrivate::KAboutKdeDialog")); 81 | } 82 | 83 | if (dialog) { 84 | setWidgetTransientParent(dialog, transientParent); 85 | dialog->windowHandle()->requestActivate(); 86 | } 87 | } 88 | 89 | #include "moc_HelpMenu.cpp" 90 | -------------------------------------------------------------------------------- /src/Platforms/ImagePlatformXcb.h: -------------------------------------------------------------------------------- 1 | /* This file is part of Spectacle, the KDE screenshot utility 2 | * SPDX-FileCopyrightText: 2019 Boudhayan Gupta <bgupta@kde.org> 3 | 4 | * SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | #pragma once 8 | 9 | #include "ImagePlatform.h" 10 | 11 | #include <xcb/xcb.h> 12 | #include <xcb/xcb_image.h> 13 | 14 | #include <QPixmap> 15 | 16 | class ImagePlatformXcb final : public ImagePlatform 17 | { 18 | Q_OBJECT 19 | 20 | public: 21 | explicit ImagePlatformXcb(QObject *parent = nullptr); 22 | ~ImagePlatformXcb() override; 23 | 24 | GrabModes supportedGrabModes() const override final; 25 | ShutterModes supportedShutterModes() const override final; 26 | 27 | public Q_SLOTS: 28 | void doGrab(ImagePlatform::ShutterMode shutterMode, 29 | ImagePlatform::GrabMode grabMode, 30 | bool includePointer, 31 | bool includeDecorations, 32 | bool includeShadow) override final; 33 | 34 | private Q_SLOTS: 35 | void updateSupportedGrabModes(); 36 | void doGrabNow(ImagePlatform::GrabMode grabMode, bool includePointer, bool includeDecorations, bool includeShadow); 37 | void doGrabOnClick(ImagePlatform::GrabMode grabMode, bool includePointer, bool includeDecorations, bool includeShadow); 38 | 39 | private: 40 | QPoint getCursorPosition(); 41 | QRect getDrawableGeometry(xcb_drawable_t drawable); 42 | xcb_window_t getWindowUnderCursor(); 43 | xcb_window_t getTransientWindowParent(xcb_window_t childWindow, QRect &windowRectOut, bool includeDecorations); 44 | 45 | /* ----------------------- Image Processing Utilities ----------------------- */ 46 | 47 | /** 48 | * @brief Adds a drop shadow to the given image. 49 | * @param image The image to add a drop shadow to. 50 | * @return The image with a drop shadow. 51 | */ 52 | QImage addDropShadow(QImage &image); 53 | 54 | QList<QRect> getScreenRects(); 55 | QImage convertFromNative(xcb_image_t *xcbImage); 56 | QImage blendCursorImage(QImage &image, const QRect rect); 57 | QImage postProcessImage(QImage &image, QRect rect, bool blendPointer); 58 | QImage getImageFromDrawable(xcb_drawable_t xcbDrawable, const QRect &rect); 59 | QImage getToplevelImage(QRect rect, bool blendPointer); 60 | QImage getWindowImage(xcb_window_t window, bool blendPointer); 61 | 62 | void grabAllScreens(bool includePointer, bool crop = false); 63 | void grabCurrentScreen(bool includePointer); 64 | void grabApplicationWindow(xcb_window_t window, bool includePointer, bool includeDecorations); 65 | void grabActiveWindow(bool includePointer, bool includeDecorations, bool includeShadow); 66 | void grabWindowUnderCursor(bool includePointer, bool includeDecorations, bool includeShadow); 67 | void grabTransientWithParent(bool includePointer, bool includeDecorations, bool includeShadow); 68 | 69 | // on-click screenshot shutter support needs a native event filter in xcb 70 | class OnClickEventFilter; 71 | std::unique_ptr<OnClickEventFilter> m_nativeEventFilter; 72 | 73 | GrabModes m_grabModes; 74 | }; 75 | -------------------------------------------------------------------------------- /snapcraft.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Scarlett Moore <sgmoore@kde.org> 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | --- 5 | name: spectacle 6 | confinement: strict 7 | grade: stable 8 | base: core22 9 | adopt-info: spectacle 10 | apps: 11 | spectacle: 12 | extensions: 13 | - kde-neon-6 14 | common-id: org.kde.spectacle.desktop 15 | desktop: usr/share/applications/org.kde.spectacle.desktop 16 | command: usr/bin/spectacle 17 | plugs: 18 | - audio-record 19 | - home 20 | - removable-media 21 | slots: 22 | session-dbus-interface: 23 | interface: dbus 24 | name: org.kde.spectacle 25 | bus: session 26 | package-repositories: 27 | - type: apt 28 | components: 29 | - main 30 | suites: 31 | - jammy 32 | key-id: 444DABCF3667D0283F894EDDE6D4736255751E5D 33 | url: http://origin.archive.neon.kde.org/user 34 | key-server: keyserver.ubuntu.com 35 | parts: 36 | spectacle: 37 | parse-info: 38 | - usr/share/metainfo/org.kde.spectacle.appdata.xml 39 | plugin: cmake 40 | source: . 41 | source-type: local 42 | build-packages: 43 | - libxcb-cursor-dev 44 | - libxcb-image0-dev 45 | - libxcb-util0-dev 46 | - libxcb-xfixes0-dev 47 | - libwayland-dev 48 | - plasma-wayland-protocols 49 | - wayland-scanner++ 50 | - libpipewire-0.3-dev 51 | stage-packages: 52 | - libxcb-image0 53 | - libxcb-cursor0 54 | - libxcb-util1 55 | - libxcb-xfixes0 56 | - kipi-plugins-common 57 | - kipi-plugins 58 | - libwayland-client0 59 | - plasma-wayland-protocols 60 | - libpipewire-0.3-0 61 | cmake-parameters: 62 | - -DCMAKE_INSTALL_PREFIX=/usr 63 | - -DCMAKE_BUILD_TYPE=Release 64 | - -DQT_MAJOR_VERSION=6 65 | - -DBUILD_WITH_QT6=ON 66 | - -DBUILD_TESTING=OFF 67 | - -DCMAKE_INSTALL_SYSCONFDIR=/etc 68 | - -DCMAKE_INSTALL_LOCALSTATEDIR=/var 69 | - -DCMAKE_EXPORT_NO_PACKAGE_REGISTRY=ON 70 | - -DCMAKE_FIND_USE_PACKAGE_REGISTRY=OFF 71 | - -DCMAKE_FIND_PACKAGE_NO_PACKAGE_REGISTRY=ON 72 | - -DCMAKE_INSTALL_RUNSTATEDIR=/run 73 | - -DCMAKE_SKIP_INSTALL_ALL_DEPENDENCY=ON 74 | - -DCMAKE_VERBOSE_MAKEFILE=ON 75 | - -DCMAKE_INSTALL_LIBDIR=lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR 76 | - --log-level=STATUS 77 | - -DCMAKE_LIBRARY_PATH=lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR 78 | prime: 79 | - -usr/lib/*/cmake/* 80 | - -usr/include/* 81 | - -usr/share/ECM/* 82 | - -usr/share/man/* 83 | - -usr/bin/X11 84 | - -usr/lib/gcc/$CRAFT_ARCH_TRIPLET_BUILD_FOR/6.0.0 85 | - -usr/lib/aspell/* 86 | - -usr/share/lintian 87 | cleanup: 88 | after: 89 | - spectacle 90 | plugin: nil 91 | build-snaps: 92 | - core22 93 | - kf6-core22 94 | - qt-common-themes 95 | override-prime: | 96 | set -eux 97 | for snap in "core22" "kf6-core22" "qt-common-themes"; do 98 | cd "/snap/$snap/current" && find . -type f,l -exec rm -rf "${CRAFT_PRIME}/{}" \; 99 | done 100 | -------------------------------------------------------------------------------- /src/Gui/ScreenshotModeMenu.cpp: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2025 Noah Davis <noahadvs@gmail.com> 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #include "ScreenshotModeMenu.h" 6 | #include "CaptureModeModel.h" 7 | #include "SpectacleCore.h" 8 | #include "ShortcutActions.h" 9 | #include <KGlobalAccel> 10 | 11 | using namespace Qt::StringLiterals; 12 | 13 | static QPointer<ScreenshotModeMenu> s_instance = nullptr; 14 | 15 | ScreenshotModeMenu::ScreenshotModeMenu(QWidget *parent) 16 | : SpectacleMenu(i18nc("@title:menu", "Screenshot Modes"), parent) 17 | { 18 | auto addModes = [this] { 19 | clear(); 20 | auto model = CaptureModeModel::instance(); 21 | for (auto idx = model->index(0); idx.isValid(); idx = idx.siblingAtRow(idx.row() + 1)) { 22 | const auto action = addAction(idx.data(Qt::DisplayRole).toString()); 23 | const auto mode = idx.data(CaptureModeModel::CaptureModeRole).value<CaptureModeModel::CaptureMode>(); 24 | QAction *globalAction = nullptr; 25 | auto globalShortcuts = [](QAction *globalAction) { 26 | if (!globalAction) { 27 | return QList<QKeySequence>{}; 28 | } 29 | auto component = ShortcutActions::self()->componentName(); 30 | auto id = globalAction->objectName(); 31 | return KGlobalAccel::self()->globalShortcut(component, id); 32 | }; 33 | switch (mode) { 34 | case CaptureModeModel::RectangularRegion: 35 | globalAction = ShortcutActions::self()->regionAction(); 36 | break; 37 | case CaptureModeModel::AllScreens: 38 | globalAction = ShortcutActions::self()->fullScreenAction(); 39 | break; 40 | case CaptureModeModel::CurrentScreen: 41 | globalAction = ShortcutActions::self()->currentScreenAction(); 42 | break; 43 | case CaptureModeModel::ActiveWindow: 44 | globalAction = ShortcutActions::self()->activeWindowAction(); 45 | break; 46 | case CaptureModeModel::WindowUnderCursor: 47 | globalAction = ShortcutActions::self()->windowUnderCursorAction(); 48 | break; 49 | case CaptureModeModel::FullScreen: 50 | globalAction = ShortcutActions::self()->fullScreenAction(); 51 | break; 52 | default: 53 | break; 54 | } 55 | action->setShortcuts(globalShortcuts(globalAction)); 56 | auto onTriggered = [mode] { 57 | SpectacleCore::instance()->takeNewScreenshot(mode); 58 | }; 59 | connect(action, &QAction::triggered, action, onTriggered); 60 | } 61 | }; 62 | addModes(); 63 | connect(CaptureModeModel::instance(), &CaptureModeModel::captureModesChanged, this, addModes); 64 | } 65 | 66 | ScreenshotModeMenu *ScreenshotModeMenu::instance() 67 | { 68 | if (!s_instance) { 69 | s_instance = new ScreenshotModeMenu; 70 | } 71 | return s_instance; 72 | } 73 | 74 | #include "moc_ScreenshotModeMenu.cpp" 75 | -------------------------------------------------------------------------------- /src/Gui/SettingsDialog/VideoSaveOptionsPage.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2019 David Redondo <kde@david-redondo.de> 3 | * SPDX-FileCopyrightText: 2015 Boudhayan Gupta <bgupta@kde.org> 4 | * 5 | * SPDX-License-Identifier: LGPL-2.0-or-later 6 | */ 7 | 8 | #include "VideoSaveOptionsPage.h" 9 | 10 | #include "Platforms/VideoPlatform.h" 11 | #include "SpectacleCore.h" 12 | #include "ExportManager.h" 13 | #include "SaveOptionsUtils.h" 14 | #include "VideoFormatComboBox.h" 15 | #include "VideoFormatModel.h" 16 | #include "ui_VideoSaveOptions.h" 17 | 18 | #include <KLocalizedString> 19 | 20 | #include <QCheckBox> 21 | #include <QComboBox> 22 | #include <QFontDatabase> 23 | #include <QImageWriter> 24 | #include <QLabel> 25 | #include <QLineEdit> 26 | 27 | using namespace Qt::StringLiterals; 28 | 29 | VideoSaveOptionsPage::VideoSaveOptionsPage(QWidget *parent) 30 | : QWidget(parent) 31 | , m_ui(new Ui_VideoSaveOptions) 32 | { 33 | m_ui->setupUi(this); 34 | 35 | m_ui->preview->setFixedHeight(m_ui->kcfg_videoFilenameTemplate->height()); 36 | 37 | m_videoFormatModel = std::make_unique<VideoFormatModel>(); 38 | m_videoFormatComboBox = std::make_unique<VideoFormatComboBox>(m_videoFormatModel.get(), this); 39 | m_ui->saveLayout->addWidget(m_videoFormatComboBox.get()); 40 | 41 | // Auto select the correct format if the user types an extension in the filename template. 42 | connect(m_ui->kcfg_videoFilenameTemplate, &QLineEdit::textEdited, this, [this](const QString &text) { 43 | const auto count = m_videoFormatModel->rowCount(); 44 | for (auto i = 0; i < count; ++i) { 45 | auto index = m_videoFormatModel->index(i); 46 | auto extension = index.data(VideoFormatModel::ExtensionRole).toString(); 47 | if (text.endsWith(u'.' + extension, Qt::CaseInsensitive)) { 48 | m_ui->kcfg_videoFilenameTemplate->setText(text.chopped(extension.length() + 1)); 49 | m_videoFormatComboBox->setCurrentIndex(i); 50 | } 51 | } 52 | }); 53 | connect(m_ui->kcfg_videoFilenameTemplate, &QLineEdit::textChanged, 54 | this, &VideoSaveOptionsPage::updateFilenamePreview); 55 | connect(m_videoFormatComboBox.get(), &QComboBox::currentTextChanged, this, &VideoSaveOptionsPage::updateFilenamePreview); 56 | 57 | m_ui->captureInstructionLabel->setText(CaptureInstructions::text(false)); 58 | connect(m_ui->captureInstructionLabel, &QLabel::linkActivated, this, [this](const QString &link) { 59 | if (link == u"showmore"_s) { 60 | m_ui->captureInstructionLabel->setText(CaptureInstructions::text(true)); 61 | } else if (link == u"showfewer"_s) { 62 | m_ui->captureInstructionLabel->setText(CaptureInstructions::text(false)); 63 | } else { 64 | m_ui->kcfg_videoFilenameTemplate->insert(link); 65 | } 66 | }); 67 | } 68 | 69 | VideoSaveOptionsPage::~VideoSaveOptionsPage() = default; 70 | 71 | void VideoSaveOptionsPage::updateFilenamePreview() 72 | { 73 | const auto extension = m_videoFormatComboBox->currentData(VideoFormatModel::ExtensionRole).toString(); 74 | const auto templateBasename = m_ui->kcfg_videoFilenameTemplate->text(); 75 | ::updateFilenamePreview(m_ui->preview, templateBasename + u'.' + extension, Settings::videoSaveLocation()); 76 | } 77 | 78 | #include "moc_VideoSaveOptionsPage.cpp" 79 | -------------------------------------------------------------------------------- /src/VideoFormatModel.cpp: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022 Noah Davis <noahadvs@gmail.com> 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #include "VideoFormatModel.h" 6 | #include "SpectacleCore.h" 7 | 8 | #include <KLocalizedString> 9 | 10 | using namespace Qt::StringLiterals; 11 | 12 | VideoFormatModel::VideoFormatModel(QObject *parent) 13 | : QAbstractListModel(parent) 14 | { 15 | m_roleNames[Qt::DisplayRole] = "display"_ba; 16 | m_roleNames[FormatRole] = "format"_ba; 17 | m_roleNames[ExtensionRole] = "extension"_ba; 18 | 19 | auto platform = SpectacleCore::instance()->videoPlatform(); 20 | connect(platform, &VideoPlatform::supportedFormatsChanged, this, [this, platform]() { 21 | setFormats(platform->supportedFormats()); 22 | }); 23 | setFormats(platform->supportedFormats()); 24 | } 25 | 26 | void VideoFormatModel::setFormats(VideoPlatform::Formats formats) 27 | { 28 | m_data.clear(); 29 | if (formats.testFlag(VideoPlatform::WebM_VP9)) { 30 | m_data.append({ 31 | i18nc("@item:inlistbox Container/encoder", "WebM/VP9"), 32 | VideoPlatform::WebM_VP9, 33 | VideoPlatform::extensionForFormat(VideoPlatform::WebM_VP9), 34 | }); 35 | } 36 | if (formats.testFlag(VideoPlatform::MP4_H264)) { 37 | m_data.append({ 38 | i18nc("@item:inlistbox Container/encoder", "MP4/H.264"), 39 | VideoPlatform::MP4_H264, 40 | VideoPlatform::extensionForFormat(VideoPlatform::MP4_H264), 41 | }); 42 | } 43 | if (formats.testFlag(VideoPlatform::WebP)) { 44 | m_data.append({ 45 | i18nc("@item:inlistbox Container/encoder", "Animated WebP (better than GIF)"), 46 | VideoPlatform::WebP, 47 | VideoPlatform::extensionForFormat(VideoPlatform::WebP), 48 | }); 49 | } 50 | if (formats.testFlag(VideoPlatform::Gif)) { 51 | m_data.append({ 52 | i18nc("@item:inlistbox Container/encoder", "GIF (compatible, but inefficient)"), 53 | VideoPlatform::Gif, 54 | VideoPlatform::extensionForFormat(VideoPlatform::Gif), 55 | }); 56 | } 57 | Q_EMIT countChanged(); 58 | } 59 | 60 | int VideoFormatModel::indexOfFormat(VideoPlatform::Format format) const 61 | { 62 | int finalIndex = -1; 63 | for (int i = 0; i < m_data.length(); ++i) { 64 | if (m_data[i].format == format) { 65 | finalIndex = i; 66 | break; 67 | } 68 | } 69 | return finalIndex; 70 | } 71 | 72 | QHash<int, QByteArray> VideoFormatModel::roleNames() const 73 | { 74 | return m_roleNames; 75 | } 76 | 77 | QVariant VideoFormatModel::data(const QModelIndex &index, int role) const 78 | { 79 | int row = index.row(); 80 | QVariant ret; 81 | if (!checkIndex(index, CheckIndexOption::IndexIsValid)) { 82 | return ret; 83 | } 84 | if (role == Qt::DisplayRole) { 85 | ret = m_data.at(row).label; 86 | } else if (role == FormatRole) { 87 | ret = m_data.at(row).format; 88 | } else if (role == ExtensionRole) { 89 | ret = m_data.at(row).extension; 90 | } 91 | return ret; 92 | } 93 | 94 | int VideoFormatModel::rowCount(const QModelIndex &parent) const 95 | { 96 | Q_UNUSED(parent) 97 | return m_data.size(); 98 | } 99 | 100 | #include "moc_VideoFormatModel.cpp" 101 | -------------------------------------------------------------------------------- /src/Platforms/VideoPlatform.cpp: -------------------------------------------------------------------------------- 1 | /* This file is part of Spectacle, the KDE screenshot utility 2 | * SPDX-FileCopyrightText: 2019 Boudhayan Gupta <bgupta@kde.org> 3 | 4 | * SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | #include "VideoPlatform.h" 8 | #include <QTimerEvent> 9 | 10 | using namespace Qt::StringLiterals; 11 | 12 | VideoPlatform::VideoPlatform(QObject *parent) 13 | : QObject(parent) 14 | { 15 | } 16 | 17 | bool VideoPlatform::isRecording() const 18 | { 19 | return m_recordingState == RecordingState::Recording; 20 | } 21 | 22 | qint64 VideoPlatform::recordedTime() const 23 | { 24 | return m_recordedTime; 25 | } 26 | 27 | void VideoPlatform::timerEvent(QTimerEvent *event) 28 | { 29 | if (event->timerId() == m_basicTimer.timerId()) { 30 | m_recordedTime = m_elapsedTimer.isValid() ? m_elapsedTimer.elapsed() : 0; 31 | Q_EMIT recordedTimeChanged(); 32 | } 33 | } 34 | 35 | QString VideoPlatform::extensionForFormat(Format format) 36 | { 37 | switch (format) { 38 | case WebM_VP9: return "webm"_L1; 39 | case MP4_H264: return "mp4"_L1; 40 | case WebP: return "webp"_L1; 41 | case Gif: return "gif"_L1; 42 | default: return {}; 43 | } 44 | } 45 | 46 | VideoPlatform::Format VideoPlatform::formatForExtension(const QString &extension) 47 | { 48 | auto lowercaseExtension = extension.toLower(); 49 | if (lowercaseExtension == "webm"_L1) { 50 | return WebM_VP9; 51 | } else if (lowercaseExtension == "mp4"_L1) { 52 | return MP4_H264; 53 | } else if (lowercaseExtension == "webp"_L1) { 54 | return WebP; 55 | } else if (lowercaseExtension == "gif"_L1) { 56 | return Gif; 57 | } else { 58 | return NoFormat; 59 | } 60 | } 61 | 62 | VideoPlatform::Format VideoPlatform::formatForPath(const QString &path) 63 | { 64 | return formatForExtension(path.mid(path.lastIndexOf(u'.') + 1)); 65 | } 66 | 67 | VideoPlatform::RecordingState VideoPlatform::recordingState() const 68 | { 69 | return m_recordingState; 70 | } 71 | 72 | VideoPlatform::RecordingMode VideoPlatform::recordingMode() const 73 | { 74 | return m_recordingMode; 75 | } 76 | 77 | void VideoPlatform::setRecordingState(RecordingState state) 78 | { 79 | if (state == m_recordingState) { 80 | return; 81 | } 82 | 83 | m_recordingState = state; 84 | if (state == RecordingState::NotRecording) { 85 | m_recordedTime = 0; 86 | m_elapsedTimer.invalidate(); 87 | m_basicTimer.stop(); 88 | } else if (state == RecordingState::Recording) { 89 | m_recordedTime = 0; 90 | m_elapsedTimer.start(); 91 | m_basicTimer.start(1000, Qt::PreciseTimer, this); 92 | } else { 93 | if (m_elapsedTimer.isValid()) { 94 | m_recordedTime = m_elapsedTimer.elapsed(); 95 | } 96 | m_elapsedTimer.invalidate(); 97 | m_basicTimer.stop(); 98 | } 99 | if (state != RecordingState::Recording) { 100 | setRecordingMode(NoRecordingModes); 101 | } 102 | m_recordingState = state; 103 | Q_EMIT recordingStateChanged(state); 104 | Q_EMIT recordedTimeChanged(); 105 | } 106 | 107 | void VideoPlatform::setRecordingMode(RecordingMode mode) 108 | { 109 | if (m_recordingMode == mode) { 110 | return; 111 | } 112 | m_recordingMode = mode; 113 | } 114 | 115 | #include "moc_VideoPlatform.cpp" 116 | -------------------------------------------------------------------------------- /src/SpectacleDBusAdapter.cpp: -------------------------------------------------------------------------------- 1 | /* This file is part of Spectacle, the KDE screenshot utility 2 | * SPDX-FileCopyrightText: 2015 Boudhayan Gupta <bgupta@kde.org> 3 | * SPDX-License-Identifier: LGPL-2.0-or-later 4 | */ 5 | 6 | #include "SpectacleDBusAdapter.h" 7 | #include "Platforms/ImagePlatform.h" 8 | #include "settings.h" 9 | 10 | SpectacleDBusAdapter::SpectacleDBusAdapter(SpectacleCore *parent) 11 | : QDBusAbstractAdaptor(parent) 12 | { 13 | setAutoRelaySignals(false); 14 | } 15 | 16 | inline SpectacleCore *SpectacleDBusAdapter::parent() const 17 | { 18 | return static_cast<SpectacleCore *>(QObject::parent()); 19 | } 20 | 21 | void SpectacleDBusAdapter::FullScreen(int includeMousePointer) 22 | { 23 | parent()->takeNewScreenshot(CaptureModeModel::AllScreens, 0, (includeMousePointer == -1) ? Settings::includePointer() : includeMousePointer, true); 24 | } 25 | 26 | void SpectacleDBusAdapter::CurrentScreen(int includeMousePointer) 27 | { 28 | parent()->takeNewScreenshot(CaptureModeModel::CurrentScreen, 0, (includeMousePointer == -1) ? Settings::includePointer() : includeMousePointer, true); 29 | } 30 | 31 | void SpectacleDBusAdapter::ActiveWindow(int includeWindowDecorations, int includeMousePointer, int includeWindowShadow) 32 | { 33 | parent()->takeNewScreenshot(CaptureModeModel::ActiveWindow, 34 | 0, 35 | (includeMousePointer == -1) ? Settings::includePointer() : includeMousePointer, 36 | includeWindowDecorations == -1 ? Settings::includeDecorations() : includeWindowDecorations, 37 | includeWindowShadow == -1 ? Settings::includeShadow() : includeWindowShadow); 38 | } 39 | 40 | void SpectacleDBusAdapter::WindowUnderCursor(int includeWindowDecorations, int includeMousePointer, int includeWindowShadow) 41 | { 42 | parent()->takeNewScreenshot(CaptureModeModel::WindowUnderCursor, 43 | 0, 44 | (includeMousePointer == -1) ? Settings::includePointer() : includeMousePointer, 45 | includeWindowDecorations == -1 ? Settings::includeDecorations() : includeWindowDecorations, 46 | includeWindowShadow == -1 ? Settings::includeShadow() : includeWindowShadow); 47 | } 48 | 49 | void SpectacleDBusAdapter::RectangularRegion(int includeMousePointer) 50 | { 51 | parent()->takeNewScreenshot(CaptureModeModel::RectangularRegion, 52 | 0, 53 | (includeMousePointer == -1) ? Settings::includePointer() : includeMousePointer, 54 | false); 55 | } 56 | 57 | void SpectacleDBusAdapter::RecordRegion(int includeMousePointer) 58 | { 59 | parent()->startRecording(VideoPlatform::Region, includeMousePointer == -1 ? Settings::videoIncludePointer() : includeMousePointer); 60 | } 61 | 62 | void SpectacleDBusAdapter::RecordScreen(int includeMousePointer) 63 | { 64 | parent()->startRecording(VideoPlatform::Screen, includeMousePointer == -1 ? Settings::videoIncludePointer() : includeMousePointer); 65 | } 66 | 67 | void SpectacleDBusAdapter::RecordWindow(int includeMousePointer) 68 | { 69 | parent()->startRecording(VideoPlatform::Window, includeMousePointer == -1 ? Settings::videoIncludePointer() : includeMousePointer); 70 | } 71 | 72 | void SpectacleDBusAdapter::OpenWithoutScreenshot() 73 | { 74 | parent()->initGuiNoScreenshot(); 75 | } 76 | 77 | #include "moc_SpectacleDBusAdapter.cpp" 78 | -------------------------------------------------------------------------------- /src/Gui/QmlUtils.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2023 Noah Davis <noahadvs@gmail.com> 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | pragma Singleton 6 | 7 | import QtQuick 8 | import QtQuick.Controls as QQC 9 | import org.kde.spectacle.private 10 | 11 | /** 12 | * A general utilities singleton for use in QML. 13 | */ 14 | Item { 15 | id: root 16 | 17 | LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft 18 | LayoutMirroring.childrenInherit: true 19 | 20 | readonly property FontMetrics fontMetrics: FontMetrics { 21 | id: fontMetrics 22 | font: Qt.application.font 23 | } 24 | 25 | readonly property real iconTextButtonHeight: iconTextButton.implicitHeight 26 | 27 | readonly property real textOnlyButtonHeight: textOnlyButton.implicitHeight 28 | 29 | readonly property real iconOnlyButtonHeight: iconOnlyButton.implicitHeight 30 | 31 | function getButtonSize(display = QQC.AbstractButton.TextBesideIcon, text = "text", 32 | iconName = "edit-copy", isButtonMenu = false) { 33 | let tb = toolButtonComponent.createObject(root, { 34 | "display": display, 35 | "text": text, 36 | "isButtonMenu": isButtonMenu, 37 | "iconName": iconName 38 | }) 39 | const size = Qt.size(tb.implicitWidth, tb.implicitHeight) 40 | tb.destroy() 41 | return size 42 | } 43 | 44 | // Get the ratio between two values. 45 | // If one or both values are not finite, not null, not undefined or zero, returns 0. 46 | function ratio(dividend, divisor) { 47 | return !Number.isFinite(dividend) || !Number.isFinite(divisor) || !dividend || !divisor ? 48 | 0 : dividend / divisor 49 | } 50 | 51 | // Basically std::clamp from C++ 52 | function clamp(value, min, max) { 53 | return Math.max(min, Math.min(value, max)) 54 | } 55 | 56 | // Get a clamped pixel value. 57 | // The default minimum is 1 physical pixel with an item scale of 1. 58 | // The default maximum is positive infinity. 59 | function clampPx(value, min = 1 / Screen.devicePixelRatio, max = Number.POSITIVE_INFINITY) { 60 | return clamp(value, min, max) 61 | } 62 | 63 | // When scaling a set of points such as a path, all points are individually multiplied. 64 | // This means scaling up translates positively and scaling down translates negatively. 65 | // This can be used to get a translation for preventing translation from scaling. 66 | function unTranslateScale(oldValue, scale) { 67 | return oldValue - oldValue * scale 68 | } 69 | 70 | Component { 71 | id: toolButtonComponent 72 | QQC.ToolButton { 73 | required property bool isButtonMenu 74 | required property string iconName 75 | icon.name: iconName 76 | text: "text" 77 | Accessible.role: isButtonMenu ? Accessible.ButtonMenu : Accessible.Button 78 | } 79 | } 80 | 81 | QQC.ToolButton { 82 | id: iconTextButton 83 | display: QQC.AbstractButton.TextBesideIcon 84 | icon.name: "edit-copy" 85 | text: "text metrics" 86 | } 87 | 88 | QQC.ToolButton { 89 | id: textOnlyButton 90 | display: QQC.AbstractButton.TextOnly 91 | text: "text metrics" 92 | } 93 | 94 | QQC.ToolButton { 95 | id: iconOnlyButton 96 | display: QQC.AbstractButton.IconOnly 97 | icon.name: "edit-copy" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/QtCV.h: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2024 Noah Davis <noahadvs@gmail.com> 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #pragma once 6 | 7 | #include <QImage> 8 | #include <opencv2/opencv.hpp> 9 | 10 | /** 11 | * Convenience functions for using OpenCV with Qt APIs. 12 | */ 13 | namespace QtCV 14 | { 15 | static constexpr int INVALID_MAT_TYPE = -1; 16 | static_assert(CV_8U == 0); 17 | static_assert(std::same_as<decltype(CV_8U), int>); 18 | 19 | inline constexpr int matType(QPixelFormat::TypeInterpretation typeInterpretation) 20 | { 21 | switch (typeInterpretation) { 22 | case QPixelFormat::UnsignedByte: 23 | return CV_8U; 24 | case QPixelFormat::UnsignedShort: 25 | return CV_16U; 26 | case QPixelFormat::FloatingPoint: 27 | return CV_32F; 28 | default: 29 | return INVALID_MAT_TYPE; 30 | } 31 | } 32 | 33 | inline constexpr int matType(QPixelFormat pixelFormat) 34 | { 35 | const auto baseType = matType(pixelFormat.typeInterpretation()); 36 | if (baseType == INVALID_MAT_TYPE) { 37 | return INVALID_MAT_TYPE; 38 | } 39 | return CV_MAKETYPE(baseType, pixelFormat.channelCount()); 40 | } 41 | 42 | // Get a cv::Mat that reuses the data of a QImage. 43 | // Use cv::Mat::clone() if the owner of the data might be destroyed before you're done using it. 44 | // Expects an image with the right format. If the image has an ARGB32 format (premultiplied or not), 45 | // it needs to be converted to BGRA. RGBX8888 and RGBA8888 formats shouldn't need to be converted. 46 | inline cv::Mat qImageToMat(QImage &image) 47 | { 48 | const auto type = matType(image.pixelFormat()); 49 | if (type == INVALID_MAT_TYPE) { 50 | return {}; 51 | } 52 | // Use the constructor with cv::Size as the first arg to avoid type ambiguity in the args. 53 | return cv::Mat(cv::Size{image.width(), image.height()}, type, image.bits(), image.bytesPerLine()); 54 | } 55 | 56 | // Same as qImageToMat, but with cv::Ptr (subclass of std::shared_ptr). 57 | inline cv::Ptr<cv::Mat> qImageToMatPtr(QImage &image) 58 | { 59 | const auto type = matType(image.pixelFormat()); 60 | if (type == INVALID_MAT_TYPE) { 61 | return nullptr; 62 | } 63 | return cv::makePtr<cv::Mat>(cv::Size{image.width(), image.height()}, type, image.bits(), image.bytesPerLine()); 64 | } 65 | 66 | // For use with filters that require odd kernel dimensions. 67 | template<typename Number> 68 | inline auto sigmaToKSize(Number value) 69 | { 70 | return cvRound(value + 1) | 1; 71 | } 72 | 73 | // Stack blur looks like Gaussian blur, but doesn't become as slow with larger kernel sizes. 74 | // Stack blur is unavailable in OpenCV versions before 4.7. 75 | // We need this because of the FreeBSD 14 CI pipeline. FreeBSD 14 is only on OpenCV 4.6. 76 | inline void stackOrGaussianBlurCompatibility(cv::InputArray &in, cv::OutputArray &out, cv::Size ksize, double sigmaX, double sigmaY = 0, [[maybe_unused]] int borderType = cv::BORDER_DEFAULT) 77 | { 78 | #if CV_MAJOR_VERSION > 4 || (CV_MAJOR_VERSION == 4 && CV_MINOR_VERSION >= 7) 79 | // Replicate the behavior of cv::GaussianBlur with automatic kernel size. 80 | const auto gaussianKSizeFactor = (in.depth() == CV_8U ? 3 : 4) * 2; 81 | if( ksize.width <= 0 && sigmaX > 0 ) { 82 | ksize.width = sigmaToKSize(sigmaX * gaussianKSizeFactor); 83 | } 84 | if( ksize.height <= 0 && sigmaY > 0 ) { 85 | ksize.height = sigmaToKSize(sigmaY * gaussianKSizeFactor); 86 | } 87 | cv::stackBlur(in, out, ksize); 88 | #else 89 | cv::GaussianBlur(in, out, ksize, sigmaX, sigmaY, borderType); 90 | #endif 91 | } 92 | } 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /src/Gui/CaptureSettingsColumn.qml: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022 Noah Davis <noahadvs@gmail.com> 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | import QtQuick 6 | import QtQuick.Layouts 7 | import QtQuick.Controls as QQC 8 | import org.kde.kirigami as Kirigami 9 | import org.kde.spectacle.private 10 | 11 | ColumnLayout { 12 | Layout.minimumWidth: delayRow.implicitWidth 13 | spacing: Kirigami.Units.mediumSpacing 14 | QQC.CheckBox { 15 | Layout.fillWidth: true 16 | text: i18n("Include mouse pointer") 17 | QQC.ToolTip.text: i18n("Show the mouse cursor in the screenshot image.") 18 | QQC.ToolTip.delay: Kirigami.Units.toolTipDelay 19 | QQC.ToolTip.visible: hovered 20 | checked: Settings.includePointer 21 | onToggled: Settings.includePointer = checked 22 | } 23 | QQC.CheckBox { 24 | Layout.fillWidth: true 25 | text: i18n("Include window titlebar and borders") 26 | QQC.ToolTip.text: i18n("Show the window title bar and border when taking a screenshot of a window.") 27 | QQC.ToolTip.delay: Kirigami.Units.toolTipDelay 28 | QQC.ToolTip.visible: hovered 29 | checked: Settings.includeDecorations 30 | onToggled: Settings.includeDecorations = checked 31 | } 32 | QQC.CheckBox { 33 | Layout.fillWidth: true 34 | text: i18n("Include window shadow") 35 | QQC.ToolTip.text: i18n("Show the window shadow when taking a screenshot of a window.") 36 | QQC.ToolTip.delay: Kirigami.Units.toolTipDelay 37 | QQC.ToolTip.visible: hovered 38 | enabled: Settings.includeDecorations 39 | checked: Settings.includeShadow 40 | onToggled: Settings.includeShadow = checked 41 | } 42 | QQC.CheckBox { 43 | Layout.fillWidth: true 44 | text: i18n("Capture the current pop-up only") 45 | visible: ImagePlatform.supportedGrabModes & ImagePlatform.TransientWithParent 46 | QQC.ToolTip.text: i18n("Capture only the current pop-up window (like a menu, tooltip etc) when taking a screenshot of a window. If disabled, the pop-up is captured along with the parent window.") 47 | QQC.ToolTip.delay: Kirigami.Units.toolTipDelay 48 | QQC.ToolTip.visible: hovered 49 | checked: Settings.transientOnly 50 | onToggled: Settings.transientOnly = checked 51 | } 52 | QQC.CheckBox { 53 | Layout.fillWidth: true 54 | text: i18n("Quit after manual Save or Copy") 55 | QQC.ToolTip.text: i18n("Quit Spectacle after manually saving or copying the image.") 56 | QQC.ToolTip.delay: Kirigami.Units.toolTipDelay 57 | QQC.ToolTip.visible: hovered 58 | checked: Settings.quitAfterSaveCopyExport 59 | onToggled: Settings.quitAfterSaveCopyExport = checked 60 | } 61 | QQC.CheckBox { 62 | id: captureOnClickCheckBox 63 | Layout.fillWidth: true 64 | text: i18n("Capture on click") 65 | visible: ImagePlatform.supportedShutterModes === (ImagePlatform.Immediate | ImagePlatform.OnClick) 66 | QQC.ToolTip.text: i18n("Wait for a mouse click before capturing the screenshot image.") 67 | QQC.ToolTip.delay: Kirigami.Units.toolTipDelay 68 | QQC.ToolTip.visible: hovered || pressed 69 | checked: ImagePlatform.supportedShutterModes & ImagePlatform.OnClick && Settings.captureOnClick 70 | onToggled: Settings.captureOnClick = checked 71 | } 72 | RowLayout { 73 | id: delayRow 74 | spacing: parent.spacing 75 | QQC.Label { 76 | text: i18n("Delay:") 77 | } 78 | DelaySpinBox { 79 | enabled: !captureOnClickCheckBox.checked 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /icons/sc-apps-spectacle.svg: -------------------------------------------------------------------------------- 1 | <svg width="48" xmlns="http://www.w3.org/2000/svg" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"> 2 | <defs> 3 | <linearGradient id="a" y1="536.8" y2="503.8" x2="0" gradientUnits="userSpaceOnUse"> 4 | <stop stop-color="#2a2c2f"/> 5 | <stop offset="1" stop-color="#424649"/> 6 | </linearGradient> 7 | <linearGradient id="b" y1="547.8" y2="536.8" x2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1 0 0 .54545 0 244)"> 8 | <stop stop-color="#c6cdd1"/> 9 | <stop offset="1" stop-color="#e0e5e7"/> 10 | </linearGradient> 11 | <linearGradient id="c" y1="5.342" x1="42.799" y2="31.357" x2="11.999" gradientUnits="userSpaceOnUse"> 12 | <stop stop-color="#4ce0c6"/> 13 | <stop offset="1" stop-color="#3b85b5"/> 14 | </linearGradient> 15 | <linearGradient id="d" y1="22.346" x1="29.855" y2="28.506" x2="54.32" gradientUnits="userSpaceOnUse"> 16 | <stop stop-color="#cc4a5e"/> 17 | <stop offset="1" stop-color="#aa478a"/> 18 | </linearGradient> 19 | <linearGradient id="e" y1="41.22" x1="24.392" y2="25.343" x2="20.643" gradientUnits="userSpaceOnUse"> 20 | <stop stop-color="#334545"/> 21 | <stop offset="1" stop-color="#536161"/> 22 | </linearGradient> 23 | <linearGradient xlink:href="#e" id="f" y1="568.8" x1="431.57" y2="562.8" x2="439.57" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-1 0 0 1 839.14-40)"/> 24 | <linearGradient xlink:href="#e" id="g" y1="40.899" x1="56.03" y2="32.475" x2="40.647" gradientUnits="userSpaceOnUse"/> 25 | <linearGradient xlink:href="#e" id="h" y1="32.15" x1="60.895" y2="27.06" x2="53.41" gradientUnits="userSpaceOnUse"/> 26 | <linearGradient xlink:href="#e" id="i" y1="36.527" x1="33.32" y2="31.348" x2="24.78" gradientUnits="userSpaceOnUse"/> 27 | <linearGradient xlink:href="#e" id="j" y1="36.765" x1="34" y2="49.571" x2="40.25" gradientUnits="userSpaceOnUse"/> 28 | </defs> 29 | <g transform="translate(-384.57-499.8)"> 30 | <g stroke-opacity=".55" stroke-width="2.8"> 31 | <path fill="url(#b)" d="m402.57 536.8v6h12v-6z"/> 32 | <rect width="48" x="384.57" y="503.8" fill="url(#a)" height="36"/> 33 | </g> 34 | <g transform="matrix(.91667 0 0 .91667 34.05 43.983)"> 35 | <path fill="url(#c)" d="m12 6v30h44v-30z" transform="matrix(1.09091 0 0 1.09091 371.48 497.25)"/> 36 | <g stroke-width="2"> 37 | <path fill="url(#d)" d="m56 6l-30.28 17.482 21.682 12.518h8.6z" transform="matrix(1.09091 0 0 1.09091 371.48 497.25)"/> 38 | <path fill="url(#e)" d="m25.75 23.416l-13.75 7.939v4.645h35.605z" transform="matrix(1.09091 0 0 1.09091 371.48 497.25)"/> 39 | <path fill="url(#f)" d="m407.83 527.57l-8.259-4.768v9.536z"/> 40 | <path fill="url(#g)" d="m40.891 32.16v3.844h6.656z" transform="matrix(1.09091 0 0 1.09091 371.48 497.25)"/> 41 | <path fill="url(#h)" d="m53.32 27.787v8.213h.916l6.654-3.842z" transform="matrix(1.09091 0 0 1.09091 349.66 497.25)"/> 42 | <path fill="url(#i)" d="m25.75 32.16v3.842h6.654z" transform="matrix(1.09091 0 0 1.09091 371.48 497.25)"/> 43 | <path fill="url(#j)" d="m40.891 32.16l-6.656 3.844h6.656z" transform="matrix(1.09091 0 0 1.09091 371.48 497.25)"/> 44 | <path fill="#aa478a" d="m399.54 522.87l33.03-19.07v9.317z"/> 45 | </g> 46 | </g> 47 | <rect width="22" x="397.57" y="541.8" stroke-opacity=".55" fill="#99a1a7" height="2" stroke-width="2.8"/> 48 | <path fill="#ffffff" stroke-width=".1" d="m387.57 506.8v5h1v-4h4v-1zm37 0v1h4v4h1v-5zm-18 7v3h-2v11h13v-11h-2v-3zm1 1h7v2h-7zm-8 2v11h4v-11zm11.5 1c2.493 0 4.5 2.01 4.5 4.5 0 2.493-2.01 4.5-4.5 4.5-2.493 0-4.5-2.01-4.5-4.5 0-2.493 2.01-4.5 4.5-4.5m0 1c-1.939 0-3.5 1.561-3.5 3.5 0 1.939 1.561 3.5 3.5 3.5 1.939 0 3.5-1.561 3.5-3.5 0-1.939-1.561-3.5-3.5-3.5m-23.5 11v5h5v-1h-4v-4zm41 0v4h-4v1h5v-5z"/> 49 | </g> 50 | </svg> 51 | -------------------------------------------------------------------------------- /src/RecordingModeModel.cpp: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022 Noah Davis <noahadvs@gmail.com> 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #include "RecordingModeModel.h" 6 | #include "SpectacleCore.h" 7 | 8 | #include <KGlobalAccel> 9 | #include <KLocalizedString> 10 | 11 | #include <QApplication> 12 | #include <QDBusConnection> 13 | #include <QDBusMessage> 14 | #include <QDBusPendingCall> 15 | #include <QDBusPendingReply> 16 | #include <QScreen> 17 | #include <qnamespace.h> 18 | 19 | using namespace Qt::StringLiterals; 20 | 21 | static std::unique_ptr<RecordingModeModel> s_instance; 22 | 23 | RecordingModeModel *RecordingModeModel::instance() 24 | { 25 | if (!s_instance) { 26 | s_instance = std::make_unique<RecordingModeModel>(); 27 | } 28 | return s_instance.get(); 29 | } 30 | 31 | RecordingModeModel::RecordingModeModel(QObject *parent) 32 | : QAbstractListModel(parent) 33 | { 34 | m_roleNames[RecordingModeRole] = "recordingMode"_ba; 35 | m_roleNames[Qt::DisplayRole] = "display"_ba; 36 | 37 | auto platform = SpectacleCore::instance()->videoPlatform(); 38 | connect(platform, &VideoPlatform::supportedRecordingModesChanged, this, [this, platform]() { 39 | setRecordingModes(platform->supportedRecordingModes()); 40 | }); 41 | setRecordingModes(platform->supportedRecordingModes()); 42 | } 43 | 44 | QHash<int, QByteArray> RecordingModeModel::roleNames() const 45 | { 46 | return m_roleNames; 47 | } 48 | 49 | QVariant RecordingModeModel::data(const QModelIndex &index, int role) const 50 | { 51 | int row = index.row(); 52 | QVariant ret; 53 | if (!checkIndex(index, CheckIndexOption::IndexIsValid)) { 54 | return ret; 55 | } 56 | if (role == RecordingModeRole) { 57 | ret = m_data.at(row).mode; 58 | } else if (role == Qt::DisplayRole) { 59 | ret = m_data.at(row).label; 60 | } 61 | return ret; 62 | } 63 | 64 | int RecordingModeModel::rowCount(const QModelIndex &parent) const 65 | { 66 | Q_UNUSED(parent) 67 | return m_data.size(); 68 | } 69 | 70 | int RecordingModeModel::indexOfRecordingMode(VideoPlatform::RecordingMode mode) const 71 | { 72 | int finalIndex = -1; 73 | for (int i = 0; i < m_data.length(); ++i) { 74 | if (m_data[i].mode == mode) { 75 | finalIndex = i; 76 | break; 77 | } 78 | } 79 | return finalIndex; 80 | } 81 | 82 | void RecordingModeModel::setRecordingModes(VideoPlatform::RecordingModes modes) 83 | { 84 | auto count = m_data.size(); 85 | m_data.clear(); 86 | if (modes & VideoPlatform::Region) { 87 | m_data.append({VideoPlatform::Region, recordingModeLabel(VideoPlatform::Region)}); 88 | } 89 | if (modes & VideoPlatform::Screen) { 90 | m_data.append({VideoPlatform::Screen, recordingModeLabel(VideoPlatform::Screen)}); 91 | } 92 | if (modes & VideoPlatform::Window) { 93 | m_data.append({VideoPlatform::Window, recordingModeLabel(VideoPlatform::Window)}); 94 | } 95 | Q_EMIT recordingModesChanged(); 96 | if (count != m_data.size()) { 97 | Q_EMIT countChanged(); 98 | } 99 | } 100 | 101 | QString RecordingModeModel::recordingModeLabel(VideoPlatform::RecordingMode mode) 102 | { 103 | switch (mode) { 104 | case VideoPlatform::RecordingMode::Region: 105 | return i18nc("@item recording mode", "Rectangular Region"); 106 | case VideoPlatform::RecordingMode::Window: 107 | return i18nc("@item recording mode", "Window"); 108 | case VideoPlatform::RecordingMode::Screen: 109 | return i18nc("@item recording mode", "Full Screen"); 110 | case VideoPlatform::RecordingMode::NoRecordingModes: 111 | break; 112 | } 113 | return QString{}; 114 | } 115 | 116 | #include "moc_RecordingModeModel.cpp" 117 | -------------------------------------------------------------------------------- /src/Platforms/PlatformLoader.cpp: -------------------------------------------------------------------------------- 1 | /* This file is part of Spectacle, the KDE screenshot utility 2 | * SPDX-FileCopyrightText: 2019 Boudhayan Gupta <bgupta@kde.org> 3 | 4 | * SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | #include "PlatformLoader.h" 8 | #include "Config.h" 9 | #include "PlasmaVersion.h" 10 | #include "ScreenShotEffect.h" 11 | 12 | #include "ImagePlatformKWin.h" 13 | #include "PlatformNull.h" 14 | #include "VideoPlatformWayland.h" 15 | 16 | #ifdef XCB_FOUND 17 | #include "ImagePlatformXcb.h" 18 | #endif 19 | 20 | #include <KLocalizedString> 21 | #include <KWindowSystem> 22 | 23 | #include <QDebug> 24 | 25 | ImagePlatformPtr getForcedImagePlatform() 26 | { 27 | // This environment variable is only for testing purposes. 28 | auto platformName = qgetenv("SPECTACLE_IMAGE_PLATFORM"); 29 | if (platformName.isEmpty()) { 30 | return nullptr; 31 | } 32 | 33 | if (platformName == ImagePlatformKWin::staticMetaObject.className()) { 34 | return std::make_unique<ImagePlatformKWin>(); 35 | } else if (platformName == ImagePlatformXcb::staticMetaObject.className()) { 36 | return std::make_unique<ImagePlatformXcb>(); 37 | } else if (platformName == ImagePlatformNull::staticMetaObject.className()) { 38 | return std::make_unique<ImagePlatformNull>(); 39 | } else if (!platformName.isEmpty()) { 40 | qWarning() << "SPECTACLE_IMAGE_PLATFORM:" << platformName << "is invalid"; 41 | } 42 | 43 | return nullptr; 44 | } 45 | 46 | ImagePlatformPtr loadImagePlatform() 47 | { 48 | if (auto platform = getForcedImagePlatform()) { 49 | return platform; 50 | } 51 | 52 | // Check XDG_SESSION_TYPE because Spectacle might be using the XCB platform via XWayland 53 | const bool isReallyX11 = KWindowSystem::isPlatformX11() && qstrcmp(qgetenv("XDG_SESSION_TYPE").constData(), "wayland") != 0; 54 | // Before KWin 5.27.8, there was an infinite loop in KWin on X11 when doing rectangle captures. 55 | // Spectacle uses CaptureScreen DBus calls to KWin for rectangle captures. 56 | if (ScreenShotEffect::isLoaded() && ScreenShotEffect::version() != ScreenShotEffect::NullVersion 57 | && (!isReallyX11 || PlasmaVersion::get() >= PlasmaVersion::check(5, 27, 8))) { 58 | return std::make_unique<ImagePlatformKWin>(); 59 | } 60 | #ifdef XCB_FOUND 61 | else if (isReallyX11) { 62 | return std::make_unique<ImagePlatformXcb>(); 63 | } 64 | #endif 65 | // If nothing else worked, return the null platform 66 | return std::make_unique<ImagePlatformNull>(); 67 | } 68 | 69 | VideoPlatformPtr getForcedVideoPlatform() 70 | { 71 | // This environment variable is only for testing purposes. 72 | auto platformName = qgetenv("SPECTACLE_VIDEO_PLATFORM"); 73 | if (platformName.isEmpty()) { 74 | return nullptr; 75 | } 76 | 77 | if (platformName == VideoPlatformWayland::staticMetaObject.className()) { 78 | return std::make_unique<VideoPlatformWayland>(); 79 | } else if (platformName == VideoPlatformNull::staticMetaObject.className()) { 80 | return std::make_unique<VideoPlatformNull>(); 81 | } else if (!platformName.isEmpty()) { 82 | qWarning() << "SPECTACLE_VIDEO_PLATFORM:" << platformName << "is invalid"; 83 | } 84 | 85 | return nullptr; 86 | } 87 | 88 | VideoPlatformPtr loadVideoPlatform() 89 | { 90 | if (auto platform = getForcedVideoPlatform()) { 91 | return platform; 92 | } 93 | if (KWindowSystem::isPlatformWayland()) { 94 | return std::make_unique<VideoPlatformWayland>(); 95 | } 96 | if (KWindowSystem::isPlatformX11()) { 97 | return std::make_unique<VideoPlatformNull>(i18nc("@info", "Screen recording is not available on X11.")); 98 | } 99 | return std::make_unique<VideoPlatformNull>(); 100 | } 101 | -------------------------------------------------------------------------------- /src/Gui/Selection.h: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022 Noah Davis <noahadvs@gmail.com> 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #pragma once 6 | 7 | #include <qqml.h> 8 | #include <QObject> 9 | #include <QRectF> 10 | 11 | class QQuickItem; 12 | class SelectionEditor; 13 | 14 | /** 15 | * This class provides information about the selected rectangle capture region and a few related utilities. 16 | * Uses logical global coordinates. 17 | */ 18 | class Selection : public QObject 19 | { 20 | Q_OBJECT 21 | QML_ELEMENT 22 | QML_UNCREATABLE("Created by SelectionEditor") 23 | 24 | // TODO: make it impossible to misuse combinations of x/y/width/height, 25 | // left/top/right/bottom and horizontalCenter/verticalCenter bindings? 26 | Q_PROPERTY(qreal x READ x WRITE setX NOTIFY xChanged FINAL) 27 | Q_PROPERTY(qreal y READ y WRITE setY NOTIFY yChanged FINAL) 28 | Q_PROPERTY(qreal width READ width WRITE setWidth NOTIFY widthChanged FINAL) 29 | Q_PROPERTY(qreal height READ height WRITE setHeight NOTIFY heightChanged FINAL) 30 | 31 | Q_PROPERTY(qreal left READ left WRITE setLeft NOTIFY leftChanged FINAL) 32 | Q_PROPERTY(qreal top READ top WRITE setTop NOTIFY topChanged FINAL) 33 | Q_PROPERTY(qreal right READ right WRITE setRight NOTIFY rightChanged FINAL) 34 | Q_PROPERTY(qreal bottom READ bottom WRITE setBottom NOTIFY bottomChanged FINAL) 35 | 36 | Q_PROPERTY(qreal horizontalCenter READ horizontalCenter WRITE setHorizontalCenter NOTIFY horizontalCenterChanged FINAL) 37 | Q_PROPERTY(qreal verticalCenter READ verticalCenter WRITE setVerticalCenter NOTIFY verticalCenterChanged FINAL) 38 | 39 | Q_PROPERTY(QRectF rect READ rectF WRITE setRect NOTIFY rectChanged FINAL) 40 | Q_PROPERTY(QSizeF size READ sizeF NOTIFY sizeChanged FINAL) 41 | 42 | Q_PROPERTY(bool empty READ isEmpty NOTIFY emptyChanged() FINAL) 43 | 44 | public: 45 | explicit Selection(SelectionEditor *editor); 46 | ~Selection() override = default; 47 | 48 | qreal x() const; 49 | void setX(qreal x); 50 | 51 | qreal y() const; 52 | void setY(qreal y); 53 | 54 | qreal width() const; 55 | void setWidth(qreal w); 56 | 57 | qreal height() const; 58 | void setHeight(qreal h); 59 | 60 | qreal left() const; 61 | void setLeft(qreal l); 62 | 63 | qreal top() const; 64 | void setTop(qreal t); 65 | 66 | qreal right() const; 67 | void setRight(qreal r); 68 | 69 | qreal bottom() const; 70 | void setBottom(qreal b); 71 | 72 | qreal horizontalCenter() const; 73 | void setHorizontalCenter(qreal hc); 74 | 75 | qreal verticalCenter() const; 76 | void setVerticalCenter(qreal vc); 77 | 78 | void moveTo(qreal x, qreal y); 79 | void moveTo(const QPointF &p); 80 | 81 | void setRect(const QRectF &r); 82 | void setRect(qreal x, qreal y, qreal w, qreal h); 83 | 84 | QRectF rectF() const; 85 | QSizeF sizeF() const; 86 | 87 | QRectF normalized() const; 88 | 89 | bool isEmpty() const; 90 | 91 | bool contains(const QPointF &p) const; 92 | 93 | Q_SIGNALS: 94 | void xChanged(); 95 | void yChanged(); 96 | void widthChanged(); 97 | void heightChanged(); 98 | 99 | void leftChanged(); 100 | void topChanged(); 101 | void rightChanged(); 102 | void bottomChanged(); 103 | 104 | void horizontalCenterChanged(); 105 | void verticalCenterChanged(); 106 | 107 | void rectChanged(); 108 | void sizeChanged(); 109 | 110 | void emptyChanged(); 111 | 112 | private: 113 | void setRect(const QRectF &newRect, Qt::Orientations orientations); 114 | 115 | QRectF selection; 116 | // mainly exists so that I don't have to qobject_cast the parent 117 | SelectionEditor *const editor; 118 | }; 119 | 120 | QDebug operator<<(QDebug debug, const Selection *selection); 121 | -------------------------------------------------------------------------------- /src/Gui/SettingsDialog/OcrLanguageSelector.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Jhair Paris <dev@jhairparis.com> 3 | * 4 | * SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | #ifndef OCRLANGUAGESELECTOR_H 8 | #define OCRLANGUAGESELECTOR_H 9 | 10 | #include <QCheckBox> 11 | #include <QVBoxLayout> 12 | #include <QWidget> 13 | 14 | class OcrManager; 15 | 16 | /** 17 | * @brief Specialized widget for OCR language selection with multi-language support 18 | * 19 | * This widget encapsulates all the logic for OCR language selection: 20 | * - Displays available languages as checkboxes (excluding 'osd') 21 | * - Enforces limits: minimum 1, maximum languages defined by OcrManager 22 | * - Handles defaults: English preferred, fallback to first available 23 | * - Follows KConfigDialog pattern: no auto-persistence, explicit save/update methods 24 | * - Updates dynamically when OCR manager state changes 25 | */ 26 | class OcrLanguageSelector : public QWidget 27 | { 28 | Q_OBJECT 29 | Q_PROPERTY(QStringList selectedLanguages READ selectedLanguages WRITE setSelectedLanguages NOTIFY selectedLanguagesChanged USER true) 30 | Q_PROPERTY(bool isDefault READ isDefault NOTIFY selectedLanguagesChanged) 31 | Q_PROPERTY(bool hasChanges READ hasChanges NOTIFY selectedLanguagesChanged) 32 | 33 | public: 34 | explicit OcrLanguageSelector(QWidget *parent = nullptr); 35 | ~OcrLanguageSelector() override; 36 | 37 | /** 38 | * @brief Get currently selected language codes 39 | * @return List of selected language codes (e.g., ["eng", "spa"]) 40 | */ 41 | QStringList selectedLanguages() const; 42 | 43 | /** 44 | * @brief Set selected languages 45 | * @param languages List of language codes to select 46 | */ 47 | void setSelectedLanguages(const QStringList &languages); 48 | 49 | /** 50 | * @brief Check if current selection is the default state 51 | * @return true if selection represents default configuration 52 | */ 53 | bool isDefault() const; 54 | 55 | /** 56 | * @brief Check if there are unsaved changes 57 | * @return true if current selection differs from saved configuration 58 | */ 59 | bool hasChanges() const; 60 | 61 | /** 62 | * @brief Apply default language selection 63 | * Selects English if available, otherwise first available language 64 | */ 65 | void applyDefaults(); 66 | 67 | /** 68 | * @brief Refresh the widget when OCR manager state changes 69 | * Rebuilds checkboxes based on current available languages 70 | */ 71 | void refresh(); 72 | 73 | /** 74 | * @brief Save current selection to settings (called by KConfigDialog) 75 | * Follows KConfigDialog pattern for saving changes 76 | */ 77 | void saveSettings(); 78 | 79 | /** 80 | * @brief Update widget to reflect current settings (called by KConfigDialog) 81 | * Reloads settings when user cancels or dialog is reopened 82 | */ 83 | void updateWidgets(); 84 | 85 | Q_SIGNALS: 86 | /** 87 | * @brief Emitted when language selection changes 88 | * @param languages New list of selected languages 89 | */ 90 | void selectedLanguagesChanged(const QStringList &languages); 91 | 92 | private Q_SLOTS: 93 | void onLanguageCheckboxChanged(); 94 | void onOcrManagerStatusChanged(); 95 | 96 | private: 97 | void setupLanguageCheckboxes(); 98 | void enforceSelectionLimits(); 99 | void updateCheckboxEnabledStates(); 100 | QString getDefaultLanguageCode() const; 101 | void blockSignalsAndSetChecked(QCheckBox *checkbox, bool checked); 102 | 103 | QVBoxLayout *m_layout; 104 | QList<QCheckBox *> m_languageCheckboxes; 105 | QMap<QString, QString> m_availableLanguages; // code -> display name 106 | bool m_blockSignals; 107 | 108 | OcrManager *m_ocrManager; 109 | }; 110 | 111 | #endif // OCRLANGUAGESELECTOR_H -------------------------------------------------------------------------------- /src/Gui/SettingsDialog/ImageSaveOptionsPage.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2019 David Redondo <kde@david-redondo.de> 3 | * SPDX-FileCopyrightText: 2015 Boudhayan Gupta <bgupta@kde.org> 4 | * 5 | * SPDX-License-Identifier: LGPL-2.0-or-later 6 | */ 7 | 8 | #include "ImageSaveOptionsPage.h" 9 | 10 | #include "ExportManager.h" 11 | #include "SaveOptionsUtils.h" 12 | #include "ui_ImageSaveOptions.h" 13 | 14 | #include <KLocalizedString> 15 | 16 | #include <QCheckBox> 17 | #include <QComboBox> 18 | #include <QFontDatabase> 19 | #include <QImageWriter> 20 | #include <QLabel> 21 | #include <QLineEdit> 22 | 23 | using namespace Qt::StringLiterals; 24 | 25 | ImageSaveOptionsPage::ImageSaveOptionsPage(QWidget *parent) 26 | : QWidget(parent) 27 | , m_ui(new Ui_ImageSaveOptions) 28 | { 29 | m_ui->setupUi(this); 30 | 31 | m_ui->imageCompressionQualityHelpLable->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); 32 | const int sliderSpinboxHeightDiff = m_ui->qualitySpinner->sizeHint().height() - m_ui->kcfg_imageCompressionQuality->sizeHint().height(); 33 | const int smallLabelLineEditHeightDiff = 34 | m_ui->kcfg_imageFilenameTemplate->sizeHint().height() - m_ui->imageCompressionQualityHelpLable->sizeHint().height(); 35 | m_ui->qualityVLayout->setContentsMargins({ 36 | 0, 37 | std::max(0, qRound(sliderSpinboxHeightDiff / 2.0)), 38 | 0, 39 | std::max(0, qRound(smallLabelLineEditHeightDiff / 2.0)), 40 | }); 41 | 42 | connect(m_ui->kcfg_imageFilenameTemplate, &QLineEdit::textEdited, this, [&](const QString &newText) { 43 | QString fmt; 44 | const auto imageFormats = QImageWriter::supportedImageFormats(); 45 | for (const auto &item : imageFormats) { 46 | fmt = QString::fromLocal8Bit(item); 47 | if (newText.endsWith(u'.' + fmt, Qt::CaseInsensitive)) { 48 | QString txtCopy = newText; 49 | txtCopy.chop(fmt.length() + 1); 50 | m_ui->kcfg_imageFilenameTemplate->setText(txtCopy); 51 | m_ui->kcfg_preferredImageFormat->setCurrentIndex(m_ui->kcfg_preferredImageFormat->findText(fmt.toUpper())); 52 | } 53 | } 54 | }); 55 | connect(m_ui->kcfg_imageFilenameTemplate, &QLineEdit::textChanged, this, &ImageSaveOptionsPage::updateFilenamePreview); 56 | 57 | m_ui->preview->setFixedHeight(m_ui->kcfg_imageFilenameTemplate->height()); 58 | 59 | m_ui->kcfg_preferredImageFormat->addItems([&]() { 60 | QStringList items; 61 | const auto formats = QImageWriter::supportedImageFormats(); 62 | items.reserve(formats.count()); 63 | for (const auto &fmt : formats) { 64 | items.append(QString::fromLocal8Bit(fmt).toUpper()); 65 | } 66 | return items; 67 | }()); 68 | connect(m_ui->kcfg_preferredImageFormat, &QComboBox::currentTextChanged, this, &ImageSaveOptionsPage::updateFilenamePreview); 69 | 70 | m_ui->captureInstructionLabel->setText(CaptureInstructions::text(false)); 71 | connect(m_ui->captureInstructionLabel, &QLabel::linkActivated, this, [this](const QString &link) { 72 | if (link == u"showmore"_s) { 73 | m_ui->captureInstructionLabel->setText(CaptureInstructions::text(true)); 74 | } else if (link == u"showfewer"_s) { 75 | m_ui->captureInstructionLabel->setText(CaptureInstructions::text(false)); 76 | } else { 77 | m_ui->kcfg_imageFilenameTemplate->insert(link); 78 | } 79 | }); 80 | } 81 | 82 | ImageSaveOptionsPage::~ImageSaveOptionsPage() = default; 83 | 84 | void ImageSaveOptionsPage::updateFilenamePreview() 85 | { 86 | const auto extension = m_ui->kcfg_preferredImageFormat->currentText().toLower(); 87 | const auto templateBasename = m_ui->kcfg_imageFilenameTemplate->text(); 88 | ::updateFilenamePreview(m_ui->preview, templateBasename + u'.' + extension, Settings::imageSaveLocation()); 89 | } 90 | 91 | #include "moc_ImageSaveOptionsPage.cpp" 92 | -------------------------------------------------------------------------------- /src/Gui/SelectionEditor.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2018 Ambareesh "Amby" Balaji <ambareeshbalaji@gmail.com> 3 | * SPDX-FileCopyrightText: 2022 Noah Davis <noahadvs@gmail.com> 4 | * SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | #ifndef SELECTIONEDITOR_H 8 | #define SELECTIONEDITOR_H 9 | 10 | #include "ExportManager.h" 11 | 12 | #include <memory> 13 | 14 | #include <QQmlEngine> 15 | 16 | #include "Selection.h" 17 | 18 | class QHoverEvent; 19 | class QKeyEvent; 20 | class QMouseEvent; 21 | class QQuickItem; 22 | class Selection; 23 | class SelectionEditorPrivate; 24 | 25 | /** 26 | * This class is used to set the selected rectangle capture region, 27 | * get information related to it and handle input from a capture window. 28 | */ 29 | class SelectionEditor : public QObject 30 | { 31 | Q_OBJECT 32 | QML_ELEMENT 33 | QML_SINGLETON 34 | 35 | Q_PROPERTY(Selection *selection READ selection CONSTANT FINAL) 36 | Q_PROPERTY(qreal devicePixelRatio READ devicePixelRatio NOTIFY devicePixelRatioChanged FINAL) 37 | Q_PROPERTY(QRectF screensRect READ screensRect NOTIFY screensRectChanged FINAL) 38 | Q_PROPERTY(Location dragLocation READ dragLocation NOTIFY dragLocationChanged FINAL) 39 | Q_PROPERTY(QRectF handlesRect READ handlesRect NOTIFY handlesRectChanged FINAL) 40 | Q_PROPERTY(QPointF mousePosition READ mousePosition NOTIFY mousePositionChanged) 41 | /// Whether or not to show the magnifier. 42 | Q_PROPERTY(bool showMagnifier READ showMagnifier NOTIFY showMagnifierChanged FINAL) 43 | /// The location that the magnifier is looking at, 44 | /// not necessarily the location of the magnifier. 45 | Q_PROPERTY(Location magnifierLocation READ magnifierLocation NOTIFY magnifierLocationChanged FINAL) 46 | 47 | public: 48 | /// Locations in relation to the current selection 49 | enum Location : short { 50 | None = 0, 51 | Outside, 52 | // clang-format off 53 | TopLeft, Top, TopRight, 54 | Left, Inside, Right, 55 | BottomLeft, Bottom, BottomRight, 56 | // clang-format on 57 | FollowMouse = Outside, // Semantic alias for the magnifier 58 | }; 59 | Q_ENUM(Location) 60 | 61 | static SelectionEditor *instance(); 62 | 63 | Selection *selection() const; 64 | 65 | qreal devicePixelRatio() const; 66 | 67 | QRectF screensRect() const; 68 | 69 | qreal screensWidth() const; 70 | qreal screensHeight() const; 71 | 72 | Location dragLocation() const; 73 | 74 | QRectF handlesRect() const; 75 | 76 | QPointF mousePosition() const; 77 | 78 | bool showMagnifier() const; 79 | 80 | Location magnifierLocation() const; 81 | 82 | Q_SLOT bool acceptSelection(ExportManager::Actions actions = {}); 83 | 84 | void reset(); 85 | 86 | static SelectionEditor *create(QQmlEngine *engine, QJSEngine *) 87 | { 88 | auto inst = instance(); 89 | Q_ASSERT(inst); 90 | Q_ASSERT(inst->thread() == engine->thread()); 91 | QJSEngine::setObjectOwnership(inst, QJSEngine::CppOwnership); 92 | return inst; 93 | } 94 | 95 | Q_SIGNALS: 96 | void devicePixelRatioChanged(); 97 | void screensRectChanged(); 98 | void dragLocationChanged(); 99 | void handlesRectChanged(); 100 | void mousePositionChanged(); 101 | void showMagnifierChanged(); 102 | void magnifierLocationChanged(); 103 | 104 | void accepted(const QRectF &rect, const ExportManager::Actions &actions); 105 | 106 | protected: 107 | bool eventFilter(QObject *watched, QEvent *event) override; 108 | void keyPressEvent(QQuickItem *item, QKeyEvent *event); 109 | void keyReleaseEvent(QQuickItem *item, QKeyEvent *event); 110 | void hoverMoveEvent(QQuickItem *item, QHoverEvent *event); 111 | void mousePressEvent(QQuickItem *item, QMouseEvent *event); 112 | void mouseMoveEvent(QQuickItem *item, QMouseEvent *event); 113 | void mouseReleaseEvent(QQuickItem *item, QMouseEvent *event); 114 | void mouseDoubleClickEvent(QQuickItem *item, QMouseEvent *event); 115 | 116 | private: 117 | friend class SelectionEditorSingleton; 118 | explicit SelectionEditor(QObject *parent = nullptr); 119 | const std::unique_ptr<SelectionEditorPrivate> d; 120 | }; 121 | 122 | #endif // SELECTIONEDITOR_H 123 | -------------------------------------------------------------------------------- /src/Gui/SettingsDialog/SettingsDialog.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Jhair Paris <dev@jhairparis.com> 3 | * SPDX-FileCopyrightText: 2019 David Redondo <kde@david-redondo.de> 4 | * SPDX-FileCopyrightText: 2015 Boudhayan Gupta <bgupta@kde.org> 5 | * 6 | * SPDX-License-Identifier: LGPL-2.0-or-later 7 | */ 8 | 9 | #include "SettingsDialog.h" 10 | 11 | #include "GeneralOptionsPage.h" 12 | #include "ImageSaveOptionsPage.h" 13 | #include "OcrLanguageSelector.h" 14 | #include "ShortcutsOptionsPage.h" 15 | #include "VideoSaveOptionsPage.h" 16 | #include "settings.h" 17 | 18 | #include <QFontDatabase> 19 | #include <QScreen> 20 | #include <QStyle> 21 | #include <QWindow> 22 | 23 | #include <KLocalizedString> 24 | #include <KShortcutWidget> 25 | 26 | using namespace Qt::StringLiterals; 27 | 28 | SettingsDialog::SettingsDialog(QWidget *parent) 29 | : KConfigDialog(parent, "settings"_L1, Settings::self()) 30 | , m_generalPage(new GeneralOptionsPage(this)) 31 | , m_imagesPage(new ImageSaveOptionsPage(this)) 32 | , m_videosPage(new VideoSaveOptionsPage(this)) 33 | , m_shortcutsPage(new ShortcutsOptionsPage(this)) 34 | { 35 | setFaceType(KPageDialog::List); 36 | addPage(m_generalPage, Settings::self(), i18nc("Settings category", "General"), "spectacle"_L1); 37 | addPage(m_imagesPage, Settings::self(), i18nc("Settings category", "Image Saving"), "image-x-generic"_L1); 38 | addPage(m_videosPage, Settings::self(), i18nc("Settings category", "Video Saving"), "video-x-generic"_L1); 39 | addPage(m_shortcutsPage, i18nc("Settings category", "Shortcuts"), "preferences-desktop-keyboard"_L1); 40 | connect(m_shortcutsPage, &ShortcutsOptionsPage::shortCutsChanged, this, [this] { 41 | updateButtons(); 42 | }); 43 | connect(m_generalPage, &GeneralOptionsPage::ocrLanguageChanged, this, [this] { 44 | updateButtons(); 45 | }); 46 | connect(this, &KConfigDialog::currentPageChanged, this, &SettingsDialog::updateButtons); 47 | } 48 | 49 | QSize SettingsDialog::sizeHint() const 50 | { 51 | // Avoid having pages that need to be scrolled, 52 | // unless size is larger than available screen height. 53 | const auto headerSize = pageWidget()->pageHeader()->sizeHint(); 54 | const auto footerSize = pageWidget()->pageFooter()->sizeHint(); 55 | auto sh = m_generalPage->sizeHint(); 56 | sh = sh.expandedTo(m_imagesPage->sizeHint()); 57 | sh = sh.expandedTo(m_videosPage->sizeHint()); 58 | sh = sh.expandedTo(m_shortcutsPage->sizeHint()); 59 | sh.rheight() += headerSize.height() + footerSize.height() 60 | + style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing) * 2; 61 | sh = KConfigDialog::sizeHint().expandedTo(sh); 62 | const auto screenHeight = screen() ? screen()->availableGeometry().height() : 0; 63 | sh.setHeight(std::min(sh.height(), screenHeight)); 64 | return sh; 65 | } 66 | 67 | void SettingsDialog::showEvent(QShowEvent *event) 68 | { 69 | auto parent = parentWidget(); 70 | bool onTop = parent && parent->windowHandle()->flags().testFlag(Qt::WindowStaysOnTopHint); 71 | windowHandle()->setFlag(Qt::WindowStaysOnTopHint, onTop); 72 | 73 | m_generalPage->refreshOcrLanguageSettings(); 74 | 75 | KConfigDialog::showEvent(event); 76 | } 77 | 78 | bool SettingsDialog::hasChanged() 79 | { 80 | return m_shortcutsPage->isModified() || m_generalPage->ocrLanguageSelector()->hasChanges() || KConfigDialog::hasChanged(); 81 | } 82 | 83 | bool SettingsDialog::isDefault() 84 | { 85 | return currentPage()->name() != i18n("Shortcuts") && m_generalPage->ocrLanguageSelector()->isDefault() && KConfigDialog::isDefault(); 86 | } 87 | 88 | void SettingsDialog::updateSettings() 89 | { 90 | KConfigDialog::updateSettings(); 91 | m_shortcutsPage->saveChanges(); 92 | 93 | m_generalPage->ocrLanguageSelector()->saveSettings(); 94 | } 95 | 96 | void SettingsDialog::updateWidgets() 97 | { 98 | KConfigDialog::updateWidgets(); 99 | m_shortcutsPage->resetChanges(); 100 | 101 | m_generalPage->ocrLanguageSelector()->updateWidgets(); 102 | m_generalPage->refreshOcrLanguageSettings(); 103 | } 104 | 105 | void SettingsDialog::updateWidgetsDefault() 106 | { 107 | KConfigDialog::updateWidgetsDefault(); 108 | m_shortcutsPage->defaults(); 109 | 110 | m_generalPage->ocrLanguageSelector()->applyDefaults(); 111 | m_generalPage->refreshOcrLanguageSettings(); 112 | } 113 | 114 | #include "moc_SettingsDialog.cpp" 115 | -------------------------------------------------------------------------------- /src/Gui/SpectacleWindow.h: -------------------------------------------------------------------------------- 1 | /* This file is part of Spectacle, the KDE screenshot utility 2 | * SPDX-FileCopyrightText: 2015 Boudhayan Gupta <bgupta@kde.org> 3 | * SPDX-FileCopyrightText: 2022 Noah Davis <noahadvs@gmail.com> 4 | * SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | #pragma once 8 | 9 | #include <QQuickView> 10 | #include <QQmlContext> 11 | 12 | class SpectacleWindowPrivate; 13 | 14 | /** 15 | * The base window class for Spectacle's Qt Quick UIs. 16 | * Adapted from KSMainWindow, a QDialog subclass from the old Qt Widgets UI. 17 | */ 18 | class SpectacleWindow : public QQuickView 19 | { 20 | Q_OBJECT 21 | Q_PROPERTY(bool annotating READ isAnnotating WRITE setAnnotating NOTIFY annotatingChanged FINAL) 22 | Q_PROPERTY(qreal logicalX READ logicalX NOTIFY logicalXChanged) 23 | Q_PROPERTY(qreal logicalY READ logicalY NOTIFY logicalYChanged) 24 | 25 | public: 26 | enum TitlePreset { 27 | Default, 28 | Timer, 29 | Unsaved, 30 | Saved, 31 | Modified, 32 | Previous, 33 | }; 34 | 35 | qreal logicalX() const; 36 | qreal logicalY() const; 37 | 38 | bool isAnnotating() const; 39 | void setAnnotating(bool annotating); 40 | 41 | /** 42 | * Makes the window visible and removes the WindowMinimized flag from the WindowStates flags. 43 | */ 44 | void unminimize(); 45 | 46 | static QList<SpectacleWindow *> instances(); 47 | 48 | /** 49 | * Set the visibility of all SpectacleWindows created in SpectacleCore. 50 | * This will not work until the windows are fully initialized in SpectacleCore. 51 | */ 52 | static void setVisibilityForAll(QWindow::Visibility visibility); 53 | 54 | /** 55 | * For all SpectacleWindows created in SpectacleCore, set the title based on the chosen preset. 56 | * The `fileName` parameter is used for the Saved and Modified presets. 57 | * This will not work until the windows are fully initialized in SpectacleCore. 58 | */ 59 | static void setTitleForAll(TitlePreset preset, const QString &fileName = {}); 60 | 61 | /** 62 | * Close all SpectacleWindows. 63 | */ 64 | static void closeAll(); 65 | 66 | /** 67 | * Round values to be physically pixel perfect, based on the device pixel ratio. 68 | * Meant to be used with coordinates, line widths and shape sizes. 69 | * This is meant to be used in QML. 70 | */ 71 | Q_INVOKABLE qreal dprRound(qreal value) const; 72 | Q_INVOKABLE QPointF dprRound(const QPointF &point) const; 73 | Q_INVOKABLE qreal dprCeil(qreal value) const; 74 | Q_INVOKABLE qreal dprFloor(qreal value) const; 75 | 76 | /** 77 | * Get the basename for a file URL. 78 | * This is meant to be used in QML. 79 | */ 80 | Q_INVOKABLE QString baseFileName(const QUrl &url) const; 81 | 82 | public Q_SLOTS: 83 | virtual void save(); 84 | virtual void saveAs(); 85 | virtual void copyImage(); 86 | virtual void copyLocation(); 87 | 88 | void showPrintDialog(); 89 | void showPreferencesDialog(); 90 | void showFontDialog(); 91 | void showColorDialog(int option); 92 | 93 | Q_SIGNALS: 94 | void annotatingChanged(); 95 | void logicalXChanged(); 96 | void logicalYChanged(); 97 | 98 | protected: 99 | explicit SpectacleWindow(QQmlEngine *engine, QWindow *parent = nullptr); 100 | ~SpectacleWindow(); 101 | 102 | using QQuickView::setTitle; 103 | 104 | static QString titlePresetString(TitlePreset preset, const QString &fileName = {}); 105 | static void deleter(SpectacleWindow *window); 106 | 107 | // set source, but with a window specific QQmlContext and initial properties 108 | void setSource(const QUrl &source, const QVariantMap &initialProperties); 109 | 110 | void mousePressEvent(QMouseEvent *event) override; 111 | void keyPressEvent(QKeyEvent *event) override; 112 | void keyReleaseEvent(QKeyEvent *event) override; 113 | 114 | static QList<SpectacleWindow *> s_spectacleWindowInstances; 115 | static bool s_synchronizingVisibility; 116 | static bool s_synchronizingTitle; 117 | static TitlePreset s_lastTitlePreset; 118 | static QString s_previousTitle; 119 | static bool s_synchronizingAnnotating; 120 | static bool s_isAnnotating; 121 | 122 | const std::unique_ptr<QQmlContext> m_context; 123 | std::unique_ptr<QQmlComponent> m_component; 124 | 125 | QKeySequence m_pressedKeys; 126 | }; 127 | -------------------------------------------------------------------------------- /src/ImageMetaData.h: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2024 Noah Davis <noahadvs@gmail.com> 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #pragma once 6 | 7 | #include <QDataStream> 8 | #include <QImage> 9 | #include <QMap> 10 | #include <QString> 11 | 12 | namespace ImageMetaData 13 | { 14 | using namespace Qt::StringLiterals; 15 | 16 | namespace Keys 17 | { 18 | static const auto windowTitle = u"windowTitle"_s; 19 | static const auto screen = u"screen"_s; 20 | // Replacement for QImage::offset since that only accepts QPoints 21 | static const QString logicalX = u"logicalX"_s; 22 | static const QString logicalY = u"logicalY"_s; 23 | static const auto subGeometryList = u"subGeometryList"_s; 24 | enum SubGeometryProperty : quint8 { X, Y, Width, Height, DevicePixelRatio }; 25 | } 26 | 27 | inline QString windowTitle(const QImage &image) 28 | { 29 | return image.text(Keys::windowTitle); 30 | } 31 | 32 | inline void setWindowTitle(QImage &image, const QString &windowTitle) 33 | { 34 | image.setText(Keys::windowTitle, windowTitle); 35 | } 36 | 37 | template<typename Map> 38 | inline QString windowTitle(const Map &map) 39 | { 40 | return map.value(Keys::windowTitle); 41 | } 42 | 43 | template<typename Map> 44 | inline void setWindowTitle(Map &map, const QString &windowTitle) 45 | { 46 | map[Keys::windowTitle] = windowTitle; 47 | } 48 | 49 | inline QString screen(const QImage &image) 50 | { 51 | return image.text(Keys::screen); 52 | } 53 | 54 | inline void setScreen(QImage &image, const QString &screen) 55 | { 56 | image.setText(Keys::screen, screen); 57 | } 58 | 59 | template<typename Map> 60 | inline QString screen(const Map &map) 61 | { 62 | return map.value(Keys::screen); 63 | } 64 | 65 | template<typename Map> 66 | inline void setScreen(Map &map, const QString &screen) 67 | { 68 | map[Keys::screen] = screen; 69 | } 70 | 71 | inline static void setLogicalXY(QImage &image, qreal x, qreal y) 72 | { 73 | image.setText(Keys::logicalX, QString::number(x)); 74 | image.setText(Keys::logicalY, QString::number(y)); 75 | } 76 | 77 | inline static QPointF logicalXY(const QImage &image) 78 | { 79 | return {image.text(Keys::logicalX).toDouble(), image.text(Keys::logicalY).toDouble()}; 80 | } 81 | 82 | using SubGeometryPropertyMap = QMap<quint8, double>; 83 | using SubGeometryList = QList<SubGeometryPropertyMap>; 84 | 85 | inline SubGeometryList subGeometryList(const QImage &image) 86 | { 87 | auto geometryListText = image.text(Keys::subGeometryList); 88 | if (geometryListText.isEmpty()) { 89 | return {}; 90 | } 91 | QDataStream stream(geometryListText.toLatin1()); 92 | // We don't need to worry about QDataStream version as long as we're 93 | // not reading serialized data from persistent storage. 94 | SubGeometryList list; 95 | stream >> list; 96 | return list; 97 | } 98 | 99 | inline void setSubGeometryList(QImage &image, const SubGeometryList &list) 100 | { 101 | if (list.empty()) { 102 | return; 103 | } 104 | QByteArray buffer; 105 | QDataStream stream(&buffer, QDataStream::WriteOnly); 106 | stream << list; 107 | image.setText(Keys::subGeometryList, QString::fromLatin1(buffer)); 108 | } 109 | 110 | inline static SubGeometryPropertyMap subGeometryPropertyMap(const QRectF &rect, qreal devicePixelRatio) 111 | { 112 | return { 113 | {Keys::SubGeometryProperty::X, rect.x()}, 114 | {Keys::SubGeometryProperty::Y, rect.y()}, 115 | {Keys::SubGeometryProperty::Width, rect.width()}, 116 | {Keys::SubGeometryProperty::Height, rect.height()}, 117 | {Keys::SubGeometryProperty::DevicePixelRatio, devicePixelRatio}, 118 | }; 119 | } 120 | 121 | inline static QRectF rectFromSubGeometryPropertyMap(const SubGeometryPropertyMap &map) 122 | { 123 | return {map.value(Keys::SubGeometryProperty::X), 124 | map.value(Keys::SubGeometryProperty::Y), 125 | map.value(Keys::SubGeometryProperty::Width), 126 | map.value(Keys::SubGeometryProperty::Height)}; 127 | } 128 | 129 | inline static void copy(QImage &target, const QImage &source) 130 | { 131 | target.setDotsPerMeterX(source.dotsPerMeterX()); 132 | target.setDotsPerMeterY(source.dotsPerMeterY()); 133 | target.setDevicePixelRatio(source.devicePixelRatio()); 134 | const auto keys = source.textKeys(); 135 | for (const auto &key : keys) { 136 | target.setText(key, source.text(key)); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Gui/InlineMessageModel.cpp: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022 Noah Davis <noahadvs@gmail.com> 2 | * SPDX-License-Identifier: LGPL-2.0-or-later 3 | */ 4 | 5 | #include "InlineMessageModel.h" 6 | 7 | #include <KIO/JobUiDelegateFactory> 8 | #include <KIO/OpenFileManagerWindowJob> 9 | #include <KSystemClipboard> 10 | #include <QMimeData> 11 | 12 | using namespace Qt::StringLiterals; 13 | 14 | static std::unique_ptr<InlineMessageModel> s_instance; 15 | 16 | InlineMessageModel *InlineMessageModel::instance() 17 | { 18 | if (!s_instance) { 19 | s_instance = std::unique_ptr<InlineMessageModel>(new InlineMessageModel()); 20 | } 21 | return s_instance.get(); 22 | } 23 | 24 | InlineMessageModel::InlineMessageModel(QObject *parent) 25 | : QAbstractListModel(parent) 26 | , m_roleNames({ 27 | {TypeRole, "type"_ba}, 28 | {Qt::DisplayRole, "text"_ba}, 29 | {DataRole, "data"_ba}, 30 | }) 31 | { 32 | } 33 | 34 | QHash<int, QByteArray> InlineMessageModel::roleNames() const 35 | { 36 | return m_roleNames; 37 | } 38 | 39 | QVariant InlineMessageModel::data(const QModelIndex &index, int role) const 40 | { 41 | int row = index.row(); 42 | QVariant ret; 43 | if (!checkIndex(index, CheckIndexOption::IndexIsValid)) { 44 | return ret; 45 | } 46 | if (role == TypeRole) { 47 | ret = m_data.at(row).type; 48 | } else if (role == Qt::DisplayRole) { 49 | ret = m_data.at(row).text; 50 | } else if (role == DataRole) { 51 | ret = m_data.at(row).data; 52 | } 53 | return ret; 54 | } 55 | 56 | int InlineMessageModel::rowCount(const QModelIndex &parent) const 57 | { 58 | Q_UNUSED(parent) 59 | return m_data.size(); 60 | } 61 | 62 | void InlineMessageModel::push(InlineMessageType type, const QString &text, const QVariant &data) 63 | { 64 | const int oldCount = m_data.size(); 65 | QModelIndex removed; 66 | if (type >= InformationalType) { 67 | for (int i = 0; i < oldCount; ++i) { 68 | // Replace info messages of the same type since info messages are more common and more likely to become irrelevant after new user actions. 69 | if (m_data[i].type == type) { 70 | removed = index(i); 71 | beginRemoveRows({}, i, i); 72 | m_data.removeAt(i); 73 | endRemoveRows(); 74 | break; 75 | } 76 | } 77 | } else { 78 | for (int i = 0; i < oldCount; ++i) { 79 | // Replace other messages that are exactly the same to prevent spam. 80 | if (m_data[i].type == type && m_data[i].text == text && m_data[i].data == data) { 81 | removed = index(i); 82 | beginRemoveRows({}, i, i); 83 | m_data.removeAt(i); 84 | endRemoveRows(); 85 | break; 86 | } 87 | } 88 | } 89 | const int i = m_data.size(); 90 | beginInsertRows({}, i, i); 91 | m_data.push_back({type, text, data}); 92 | endInsertRows(); 93 | auto last = index(i); 94 | if (removed.isValid()) { 95 | Q_EMIT dataChanged(removed, last, {TypeRole, Qt::DisplayRole, DataRole}); 96 | } 97 | if (oldCount != m_data.size()) { 98 | Q_EMIT countChanged(); 99 | } 100 | } 101 | 102 | void InlineMessageModel::pop(int row) 103 | { 104 | if (row == -1) { 105 | row = m_data.size() - 1; 106 | } 107 | auto modelIndex = index(row); 108 | if (!modelIndex.isValid()) { 109 | return; 110 | } 111 | 112 | beginRemoveRows({}, row, row); 113 | m_data.removeAt(row); 114 | endRemoveRows(); 115 | Q_EMIT countChanged(); 116 | } 117 | 118 | void InlineMessageModel::clear() 119 | { 120 | if (m_data.empty()) { 121 | return; 122 | } 123 | 124 | beginRemoveRows({}, 0, m_data.size() - 1); 125 | m_data = {}; 126 | endRemoveRows(); 127 | Q_EMIT countChanged(); 128 | } 129 | 130 | void InlineMessageModel::copyToClipboard(const QVariant &content) 131 | { 132 | auto data = new QMimeData(); 133 | if (content.typeId() == QMetaType::QString) { 134 | data->setText(content.toString()); 135 | } else if (content.typeId() == QMetaType::QByteArray) { 136 | data->setData(QStringLiteral("application/octet-stream"), content.toByteArray()); 137 | } 138 | KSystemClipboard::instance()->setMimeData(data, QClipboard::Clipboard); 139 | } 140 | 141 | void InlineMessageModel::openContainingFolder(const QUrl &url) 142 | { 143 | KIO::highlightInFileManager({url}); 144 | } 145 | 146 | #include "moc_InlineMessageModel.cpp" 147 | --------------------------------------------------------------------------------