From 1ba4cb4cc2c5491873f317b57345809e83a2ef81 Mon Sep 17 00:00:00 2001 From: alin m elena Date: Mon, 31 Aug 2026 23:02:08 +0100 Subject: [PATCH] add laster pointer in addition to the spotlight --- doc/USER-GUIDE.md | 43 +++- plasma/projecteurapplet.cc | 16 ++ plasma/projecteurapplet.h | 6 + plasma/qml/FullRepresentation.qml | 33 ++- qml/main.qml | 192 ++++++++++++++++- src/actiondelegate.cc | 46 ++++ src/deviceinput.cc | 14 ++ src/deviceinput.h | 24 +++ src/inputmapconfig.cc | 6 + src/main.cc | 6 +- src/org.projecteur.Projecteur.xml | 11 + src/preferencesdlg.cc | 347 ++++++++++++++++++++++++++---- src/preferencesdlg.h | 6 + src/projecteurapp.cc | 50 +++++ src/projecteurconfig.kcfg | 53 +++++ src/projecteurcontrol.cc | 43 ++++ src/projecteurcontrol.h | 6 + src/settings.cc | 334 ++++++++++++++++++++++++++++ src/settings.h | 73 +++++++ src/spotlight.cc | 17 ++ 20 files changed, 1270 insertions(+), 56 deletions(-) diff --git a/doc/USER-GUIDE.md b/doc/USER-GUIDE.md index f73de74b..90d660b5 100644 --- a/doc/USER-GUIDE.md +++ b/doc/USER-GUIDE.md @@ -20,6 +20,15 @@ Applications → Projecteur**. Projecteur stores its settings in `~/.config/projecteurrc`. +## Pointer modes: Spotlight & Laser Pointer + +Projecteur supports two primary on-screen pointer modes: + +1. **Spotlight Mode**: Dims the background display and highlights the area of interest using configurable shapes (circle, square, star, ngon), borders, center dot, and live KPipeWire magnification. +2. **Laser Pointer Mode**: Projects a crisp, vibrant digital laser dot with an optical diode hotspot core and an optional glowing halo / aura. Does not dim the screen or hide background elements. + +Switch modes easily from the Plasma applet menu, **Preferences → Active Pointer Mode**, global shortcuts, presenter button actions, or CLI/D-Bus commands. + ## Spotlight and presets Under **Preferences → Spotlight**, you can configure: @@ -32,13 +41,22 @@ Under **Preferences → Spotlight**, you can configure: - multi-screen behavior. Save combinations as presets when different situations need different treatment: -for example, a small dot for slides, a large text magnifier for a code demo, and -a borderless highlight for video. +for example, a small dot for slides, a large text magnifier for a code demo, a +glowing red laser pointer for annotations, and a borderless highlight for video. +Presets remember whether they are configured for Spotlight or Laser Pointer mode. Presets are ordered alphabetically when Projecteur starts. Prefix names with numbers if you want a fixed cycle order, such as `1 Slides`, `2 Demo`, and `3 Questions`. +## Laser pointer customization + +Under **Preferences → Laser Pointer**, you can configure: + +- **Laser Dot**: Dot size (in pixels), color (any RGB/HTML color), and opacity. +- **Laser Glow / Halo**: Enable or disable the outer soft glowing aura, glow size, glow color, and glow opacity. +- **Laser Trail Mark**: Leave an optical fading persistence trace behind pointer movement for gesturing, circling, and underlining slides. Configure trail duration (100–2000 ms), stroke width (1–30 px), color, and opacity. + ## Live zoom modes Zoom uses a low-latency stream from KWin through KPipeWire. Videos, animations, @@ -61,16 +79,18 @@ only from the build directory cannot use the normal zoom path. Projecteur registers native KDE global actions for: -- toggling the spotlight; -- opening preferences; -- starting or resetting the presentation timer; -- selecting the next or previous preset. +- toggling the spotlight (`toggle_spotlight`); +- toggling the laser pointer (`toggle_laser`); +- toggling between pointer modes (`toggle_pointer_mode`); +- opening preferences (`show_preferences`); +- starting or resetting the presentation timer (`start_restart_timer`, `reset_timer`); +- selecting the next or previous preset (`next_preset`, `previous_preset`). No key combinations are assigned by default. Set them under **Preferences → Shortcuts** or **System Settings → Keyboard → Shortcuts → Projecteur**. This also makes Projecteur useful without presenter hardware: assign **Toggle -Spotlight**, then use it while sharing your screen in a meeting or recording a +Spotlight** or **Toggle Laser Pointer**, then use it while sharing your screen in a meeting or recording a demo. ## Presentation timer @@ -87,7 +107,10 @@ can provide configurable haptic feedback. Projecteur can map device input to: - a keyboard sequence; -- the next or previous spotlight preset; +- toggling the spotlight overlay; +- toggling the laser pointer; +- toggling between pointer modes (Spotlight / Laser); +- cycling or selecting presets; - vertical or horizontal scrolling; - volume control; - other built-in presentation actions. @@ -119,6 +142,10 @@ Projecteur can control an already running instance from scripts. Common examples ```sh projecteur --command spot=toggle +projecteur --command laser=toggle +projecteur --command pointer=laser +projecteur --command pointer=toggle +projecteur --command laser.size.adjust=+5 projecteur --command settings=show projecteur --command preset="2 Demo" ``` diff --git a/plasma/projecteurapplet.cc b/plasma/projecteurapplet.cc index 1373903b..c00db0f4 100644 --- a/plasma/projecteurapplet.cc +++ b/plasma/projecteurapplet.cc @@ -47,6 +47,11 @@ void ProjecteurApplet::setSpotlightActive(bool active) if (m_interface) { m_interface->SetSpotlightActive(active); } } +void ProjecteurApplet::setPointerMode(const QString& mode) +{ + if (m_interface) { m_interface->SetPointerMode(mode); } +} + void ProjecteurApplet::loadPreset(const QString& preset) { if (m_interface) { m_interface->LoadPreset(preset); } @@ -129,6 +134,13 @@ void ProjecteurApplet::remoteSpotlightActiveChanged(bool active) emit spotlightActiveChanged(); } +void ProjecteurApplet::remotePointerModeChanged(const QString& mode) +{ + if (m_pointerMode == mode) { return; } + m_pointerMode = mode; + emit pointerModeChanged(); +} + void ProjecteurApplet::remoteConnectedDevicesChanged(const QStringList& devices) { if (m_connectedDevices == devices) { return; } @@ -203,6 +215,8 @@ void ProjecteurApplet::createInterface() this, &ProjecteurApplet::remoteOverlayEnabledChanged); connect(m_interface, &OrgProjecteurProjecteurInterface::spotlightActiveChanged, this, &ProjecteurApplet::remoteSpotlightActiveChanged); + connect(m_interface, &OrgProjecteurProjecteurInterface::pointerModeChanged, + this, &ProjecteurApplet::remotePointerModeChanged); connect(m_interface, &OrgProjecteurProjecteurInterface::connectedDevicesChanged, this, &ProjecteurApplet::remoteConnectedDevicesChanged); connect(m_interface, @@ -236,6 +250,7 @@ void ProjecteurApplet::refresh() } remoteOverlayEnabledChanged(m_interface->overlayEnabled()); remoteSpotlightActiveChanged(m_interface->spotlightActive()); + remotePointerModeChanged(m_interface->pointerMode()); remoteConnectedDevicesChanged(m_interface->connectedDevices()); remoteConnectedDeviceBatteryLevelsChanged(m_interface->connectedDeviceBatteryLevels()); remoteConnectedDeviceBatteryStatusesChanged(m_interface->connectedDeviceBatteryStatuses()); @@ -259,6 +274,7 @@ void ProjecteurApplet::resetState() } remoteOverlayEnabledChanged(true); remoteSpotlightActiveChanged(false); + remotePointerModeChanged(QStringLiteral("spotlight")); remoteConnectedDevicesChanged({}); remoteConnectedDeviceBatteryLevelsChanged({}); remoteConnectedDeviceBatteryStatusesChanged({}); diff --git a/plasma/projecteurapplet.h b/plasma/projecteurapplet.h index 3e9471c4..5d9cc7af 100644 --- a/plasma/projecteurapplet.h +++ b/plasma/projecteurapplet.h @@ -18,6 +18,7 @@ class ProjecteurApplet : public Plasma::Applet Q_PROPERTY(bool trayVisible READ trayVisible NOTIFY trayVisibleChanged) Q_PROPERTY(bool overlayEnabled READ overlayEnabled NOTIFY overlayEnabledChanged) Q_PROPERTY(bool spotlightActive READ spotlightActive NOTIFY spotlightActiveChanged) + Q_PROPERTY(QString pointerMode READ pointerMode NOTIFY pointerModeChanged) Q_PROPERTY(QStringList connectedDevices READ connectedDevices NOTIFY connectedDevicesChanged) Q_PROPERTY(QList connectedDeviceBatteryLevels READ connectedDeviceBatteryLevels NOTIFY connectedDeviceBatteryLevelsChanged) @@ -38,6 +39,7 @@ class ProjecteurApplet : public Plasma::Applet bool trayVisible() const { return m_trayVisible; } bool overlayEnabled() const { return m_overlayEnabled; } bool spotlightActive() const { return m_spotlightActive; } + QString pointerMode() const { return m_pointerMode; } QStringList connectedDevices() const { return m_connectedDevices; } QList connectedDeviceBatteryLevels() const { return m_connectedDeviceBatteryLevels; } QStringList connectedDeviceBatteryStatuses() const { return m_connectedDeviceBatteryStatuses; } @@ -50,6 +52,7 @@ class ProjecteurApplet : public Plasma::Applet Q_INVOKABLE void setOverlayEnabled(bool enabled); Q_INVOKABLE void setSpotlightActive(bool active); + Q_INVOKABLE void setPointerMode(const QString& mode); Q_INVOKABLE void loadPreset(const QString& preset); Q_INVOKABLE void setTimerEnabled(bool enabled); Q_INVOKABLE void startTimer(); @@ -66,6 +69,7 @@ class ProjecteurApplet : public Plasma::Applet void trayVisibleChanged(); void overlayEnabledChanged(); void spotlightActiveChanged(); + void pointerModeChanged(); void connectedDevicesChanged(); void connectedDeviceBatteryLevelsChanged(); void connectedDeviceBatteryStatusesChanged(); @@ -81,6 +85,7 @@ private slots: void serviceUnregistered(const QString& service); void remoteOverlayEnabledChanged(bool enabled); void remoteSpotlightActiveChanged(bool active); + void remotePointerModeChanged(const QString& mode); void remoteConnectedDevicesChanged(const QStringList& devices); void remoteConnectedDeviceBatteryLevelsChanged(const QList& levels); void remoteConnectedDeviceBatteryStatusesChanged(const QStringList& statuses); @@ -103,6 +108,7 @@ private slots: bool m_trayVisible = true; bool m_overlayEnabled = true; bool m_spotlightActive = false; + QString m_pointerMode = QStringLiteral("spotlight"); QStringList m_connectedDevices; QList m_connectedDeviceBatteryLevels; QStringList m_connectedDeviceBatteryStatuses; diff --git a/plasma/qml/FullRepresentation.qml b/plasma/qml/FullRepresentation.qml index 9a875995..d94c9ce0 100644 --- a/plasma/qml/FullRepresentation.qml +++ b/plasma/qml/FullRepresentation.qml @@ -307,7 +307,7 @@ PlasmaExtras.Representation { spacing: Kirigami.Units.smallSpacing PlasmaComponents3.Label { - text: i18n("Spotlight") + text: i18n("Pointer Controls") font.bold: true } @@ -320,6 +320,29 @@ PlasmaExtras.Representation { Layout.fillWidth: true spacing: Kirigami.Units.smallSpacing + RowLayout { + Layout.fillWidth: true + spacing: Kirigami.Units.smallSpacing + + PlasmaComponents3.Label { + text: i18n("Mode:") + } + + PlasmaComponents3.ComboBox { + id: modeCombo + + Layout.fillWidth: true + enabled: root.backend && root.backend.serviceAvailable + model: [i18n("Spotlight"), i18n("Laser Pointer")] + currentIndex: (root.backend && root.backend.pointerMode === "laser") ? 1 : 0 + onActivated: (index) => { + if (root.backend) { + root.backend.setPointerMode(index === 1 ? "laser" : "spotlight"); + } + } + } + } + RowLayout { Layout.fillWidth: true spacing: Kirigami.Units.smallSpacing @@ -355,7 +378,13 @@ PlasmaExtras.Representation { PlasmaComponents3.Button { Layout.fillWidth: true enabled: root.backend && root.backend.serviceAvailable - text: root.backend && root.backend.spotlightActive ? i18n("Hide Spotlight") : i18n("Test Spotlight") + text: { + const isLaser = root.backend && root.backend.pointerMode === "laser"; + if (root.backend && root.backend.spotlightActive) { + return isLaser ? i18n("Hide Laser") : i18n("Hide Spotlight"); + } + return isLaser ? i18n("Test Laser") : i18n("Test Spotlight"); + } icon.name: root.backend && root.backend.spotlightActive ? "visibility-hidden-symbolic" : "visibility-symbolic" onClicked: root.backend.setSpotlightActive(!root.backend.spotlightActive) } diff --git a/qml/main.qml b/qml/main.qml index 34efc61c..ada8ebd7 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -120,7 +120,7 @@ Window { } MultiEffect { - visible: Settings.zoomEnabled && mainWindow.spotOnCurrentWindow + visible: Settings.pointerMode === "spotlight" && Settings.zoomEnabled && mainWindow.spotOnCurrentWindow anchors.fill: centerRect source: Settings.zoomMode === "text" ? textZoom : desktopTexture maskEnabled: true @@ -152,6 +152,136 @@ Window { mainWindow.contentItem.mapToGlobal(mouse.x, mouse.y)) } } + onPosXChanged: { + if (Settings.pointerMode === "laser" && Settings.laserTrail && ProjecteurApp.overlayVisible) { + laserTrailCanvas.addPoint(posX, posY); + } + } + onPosYChanged: { + if (Settings.pointerMode === "laser" && Settings.laserTrail && ProjecteurApp.overlayVisible) { + laserTrailCanvas.addPoint(posX, posY); + } + } + } + } + + Connections { + target: ProjecteurApp + function onOverlayVisibleChanged(visible) { + if (!visible) { + laserTrailCanvas.clear(); + } + } + } + + Connections { + target: Settings + function onPointerModeChanged() { + laserTrailCanvas.clear(); + } + function onLaserTrailChanged(trail) { + if (!trail) { + laserTrailCanvas.clear(); + } + } + } + + Canvas { + id: laserTrailCanvas + anchors.fill: parent + visible: Settings.pointerMode === "laser" && Settings.laserTrail && ProjecteurApp.overlayVisible + enabled: false + antialiasing: true + renderTarget: Canvas.FramebufferObject + renderStrategy: Canvas.Threaded + + property var points: [] + + function addPoint(x, y) { + var now = Date.now(); + if (points.length > 0) { + var last = points[points.length - 1]; + var dx = x - last.x; + var dy = y - last.y; + if (dx * dx + dy * dy < 2) { + return; + } + } + points.push({ x: x, y: y, time: now }); + if (!decayTimer.running) { + decayTimer.start(); + } + requestPaint(); + } + + function clear() { + points = []; + decayTimer.stop(); + requestPaint(); + } + + Timer { + id: decayTimer + interval: 16 + repeat: true + running: false + onTriggered: { + var now = Date.now(); + var trailTime = Math.max(50, Settings.laserTrailTime); + while (laserTrailCanvas.points.length > 0 && (now - laserTrailCanvas.points[0].time) > trailTime) { + laserTrailCanvas.points.shift(); + } + laserTrailCanvas.requestPaint(); + if (laserTrailCanvas.points.length === 0) { + decayTimer.stop(); + } + } + } + + onPaint: { + var ctx = getContext("2d"); + ctx.clearRect(0, 0, width, height); + + var pts = points; + if (!pts || pts.length < 2) { + return; + } + + var now = Date.now(); + var trailTime = Math.max(50, Settings.laserTrailTime); + var baseWidth = Math.max(1, Settings.laserTrailWidth); + var baseOpacity = Settings.laserTrailOpacity; + var trailColor = Settings.laserTrailColor; + var r = Math.round(trailColor.r * 255); + var g = Math.round(trailColor.g * 255); + var b = Math.round(trailColor.b * 255); + + ctx.lineCap = "round"; + ctx.lineJoin = "round"; + + for (var i = 1; i < pts.length; ++i) { + var p0 = pts[i - 1]; + var p1 = pts[i]; + var age = (now - p1.time) / trailTime; + if (age > 1.0) continue; + var life = Math.max(0.0, Math.min(1.0, 1.0 - age)); + + if (Settings.laserGlow && baseWidth > 2) { + ctx.beginPath(); + ctx.moveTo(p0.x, p0.y); + ctx.lineTo(p1.x, p1.y); + ctx.lineWidth = (baseWidth + 4) * life; + ctx.strokeStyle = "rgba(" + r + "," + g + "," + b + "," + (0.35 * baseOpacity * life) + ")"; + ctx.stroke(); + } + + ctx.beginPath(); + ctx.moveTo(p0.x, p0.y); + ctx.lineTo(p1.x, p1.y); + ctx.lineWidth = Math.max(0.5, baseWidth * life); + ctx.strokeStyle = "rgba(" + r + "," + g + "," + b + "," + (baseOpacity * life) + ")"; + ctx.stroke(); + } } } @@ -180,7 +310,7 @@ Window { MultiEffect { id: spot - visible: Settings.showSpotShade + visible: Settings.pointerMode === "spotlight" && Settings.showSpotShade opacity: centerRect.opacity anchors.fill: centerRect source: centerRect @@ -225,7 +355,7 @@ Window { MultiEffect { id: spotBorder - visible: Settings.showBorder && Settings.borderSize > 0 + visible: Settings.pointerMode === "spotlight" && Settings.showBorder && Settings.borderSize > 0 opacity: Settings.borderOpacity anchors.fill: centerRect source: borderShapeLoader @@ -242,11 +372,65 @@ Window { width: Settings.dotSize; height: width radius: width*0.5 color: Settings.dotColor - visible: Settings.showCenterDot + visible: Settings.pointerMode === "spotlight" && Settings.showCenterDot opacity: Settings.dotOpacity enabled: false } + Item { + id: laserPointerItem + visible: Settings.pointerMode === "laser" + anchors.centerIn: centerRect + width: Settings.laserSize + (Settings.laserGlow ? Settings.laserGlowSize * 2 : 0) + height: width + enabled: false + + Rectangle { + id: laserGlowRing + visible: Settings.laserGlow && Settings.laserGlowSize > 0 + anchors.centerIn: parent + width: parent.width + height: width + radius: width * 0.5 + color: Settings.laserGlowColor + opacity: Settings.laserGlowOpacity * 0.35 + antialiasing: true + } + + Rectangle { + id: laserMidGlow + visible: Settings.laserGlow && Settings.laserGlowSize > 0 + anchors.centerIn: parent + width: Settings.laserSize + Settings.laserGlowSize + height: width + radius: width * 0.5 + color: Settings.laserGlowColor + opacity: Settings.laserGlowOpacity * 0.7 + antialiasing: true + } + + Rectangle { + id: laserCore + anchors.centerIn: parent + width: Settings.laserSize + height: width + radius: width * 0.5 + color: Settings.laserColor + opacity: Settings.laserOpacity + antialiasing: true + + Rectangle { + anchors.centerIn: parent + width: Math.max(2, parent.width * 0.35) + height: width + radius: width * 0.5 + color: "#ffffff" + opacity: 0.8 * Settings.laserOpacity + antialiasing: true + } + } + } + Rectangle { id: topRect visible: spot.visible diff --git a/src/actiondelegate.cc b/src/actiondelegate.cc index b445fadf..cd28f707 100644 --- a/src/actiondelegate.cc +++ b/src/actiondelegate.cc @@ -66,6 +66,36 @@ namespace { } } // end namespace togglespotlight + namespace togglelaser { + // --------------------------------------------------------------------------------------------- + void paint(QPainter* p, const QStyleOptionViewItem& option, const ToggleLaserAction* /*action*/) + { + const auto& fm = option.fontMetrics; + const int xPos = (option.rect.height()-fm.height()) / 2; + NativeKeySeqEdit::drawText(xPos, *p, option, i18n("Toggle Laser Pointer")); + } + + // --------------------------------------------------------------------------------------------- + QSize sizeHint(const QStyleOptionViewItem& /*opt*/, const ToggleLaserAction* /*action*/) { + return { 100, 16 }; + } + } // end namespace togglelaser + + namespace togglepointermode { + // --------------------------------------------------------------------------------------------- + void paint(QPainter* p, const QStyleOptionViewItem& option, const TogglePointerModeAction* /*action*/) + { + const auto& fm = option.fontMetrics; + const int xPos = (option.rect.height()-fm.height()) / 2; + NativeKeySeqEdit::drawText(xPos, *p, option, i18n("Toggle Pointer Mode")); + } + + // --------------------------------------------------------------------------------------------- + QSize sizeHint(const QStyleOptionViewItem& /*opt*/, const TogglePointerModeAction* /*action*/) { + return { 100, 16 }; + } + } // end namespace togglepointermode + namespace scrollhorizontal { // --------------------------------------------------------------------------------------------- void paint(QPainter* p, const QStyleOptionViewItem& option, const ScrollHorizontalAction* /*action*/) @@ -136,6 +166,12 @@ void ActionDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option case Action::Type::ToggleSpotlight: togglespotlight::paint(painter, option, static_cast(item.action.get())); break; + case Action::Type::ToggleLaser: + togglelaser::paint(painter, option, static_cast(item.action.get())); + break; + case Action::Type::TogglePointerMode: + togglepointermode::paint(painter, option, static_cast(item.action.get())); + break; case Action::Type::ScrollHorizontal: scrollhorizontal::paint(painter, option, static_cast(item.action.get())); break; @@ -169,6 +205,10 @@ QSize ActionDelegate::sizeHint(const QStyleOptionViewItem& opt, const QModelInde return cyclepresets::sizeHint(opt, static_cast(item.action.get())); case Action::Type::ToggleSpotlight: return togglespotlight::sizeHint(opt, static_cast(item.action.get())); + case Action::Type::ToggleLaser: + return togglelaser::sizeHint(opt, static_cast(item.action.get())); + case Action::Type::TogglePointerMode: + return togglepointermode::sizeHint(opt, static_cast(item.action.get())); case Action::Type::ScrollHorizontal: return scrollhorizontal::sizeHint(opt, static_cast(item.action.get())); case Action::Type::ScrollVertical: @@ -193,6 +233,8 @@ QWidget* ActionDelegate::createEditor(QWidget* parent, const Action* action) con } case Action::Type::CyclePresets: // [[fallthrough]]; case Action::Type::ToggleSpotlight: // [[fallthrough]]; + case Action::Type::ToggleLaser: // [[fallthrough]]; + case Action::Type::TogglePointerMode:// [[fallthrough]]; case Action::Type::ScrollHorizontal: // [[fallthrough]]; case Action::Type::ScrollVertical: // [[fallthrough]]; case Action::Type::VolumeControl: @@ -315,6 +357,8 @@ void ActionTypeDelegate::paint(QPainter* painter, const QStyleOptionViewItem& op case Action::Type::KeySequence: return QChar(static_cast(Font::Icon::keyboard_4)); case Action::Type::CyclePresets: return QChar(static_cast(Font::Icon::connection_8)); case Action::Type::ToggleSpotlight: return QChar(static_cast(Font::Icon::power_on_off_11)); + case Action::Type::ToggleLaser: return QChar(static_cast(Font::Icon::target_8)); + case Action::Type::TogglePointerMode: return QChar(static_cast(Font::Icon::control_panel_9)); case Action::Type::ScrollHorizontal: return QChar(static_cast(Font::Icon::cursor_21_rotated)); case Action::Type::ScrollVertical: return QChar(static_cast(Font::Icon::cursor_21)); case Action::Type::VolumeControl: return QChar(static_cast(Font::Icon::audio_6)); @@ -352,6 +396,8 @@ void ActionTypeDelegate::actionContextMenu(QWidget* parent, InputMapConfigModel* {Action::Type::KeySequence, QChar(static_cast(Font::Icon::keyboard_4)), i18n("Key Sequence"), false}, {Action::Type::CyclePresets, QChar(static_cast(Font::Icon::connection_8)), i18n("Cycle Presets"), false}, {Action::Type::ToggleSpotlight, QChar(static_cast(Font::Icon::power_on_off_11)), i18n("Toggle Spotlight"), false}, + {Action::Type::ToggleLaser, QChar(static_cast(Font::Icon::target_8)), i18n("Toggle Laser Pointer"), false}, + {Action::Type::TogglePointerMode, QChar(static_cast(Font::Icon::control_panel_9)), i18n("Toggle Pointer Mode"), false}, {Action::Type::ScrollHorizontal, QChar(static_cast(Font::Icon::cursor_21_rotated)), i18n("Scroll Horizontal"), true}, {Action::Type::ScrollVertical, QChar(static_cast(Font::Icon::cursor_21)), i18n("Scroll Vertical"), true}, {Action::Type::VolumeControl, QChar(static_cast(Font::Icon::audio_6)), i18n("Volume Control"), true}, diff --git a/src/deviceinput.cc b/src/deviceinput.cc index 1f0f3f7f..7a11a66d 100644 --- a/src/deviceinput.cc +++ b/src/deviceinput.cc @@ -173,6 +173,12 @@ QDataStream& operator>>(QDataStream& s, MappedAction& mia) case Action::Type::ToggleSpotlight: mia.action = std::make_shared(); return mia.action->load(s); + case Action::Type::ToggleLaser: + mia.action = std::make_shared(); + return mia.action->load(s); + case Action::Type::TogglePointerMode: + mia.action = std::make_shared(); + return mia.action->load(s); case Action::Type::ScrollHorizontal: mia.action = std::make_shared(); return mia.action->load(s); @@ -203,6 +209,12 @@ bool MappedAction::operator==(const MappedAction& o) const case Action::Type::ToggleSpotlight: return (*static_cast(action.get())) == (*static_cast(o.action.get())); + case Action::Type::ToggleLaser: + return (*static_cast(action.get())) + == (*static_cast(o.action.get())); + case Action::Type::TogglePointerMode: + return (*static_cast(action.get())) + == (*static_cast(o.action.get())); case Action::Type::ScrollHorizontal: return (*static_cast(action.get())) == (*static_cast(o.action.get())); @@ -551,6 +563,8 @@ const char* toString(Action::Type at, bool withClass) ENUM_CASE_STRINGIFY3(Type, KeySequence, withClass); ENUM_CASE_STRINGIFY3(Type, CyclePresets, withClass); ENUM_CASE_STRINGIFY3(Type, ToggleSpotlight, withClass); + ENUM_CASE_STRINGIFY3(Type, ToggleLaser, withClass); + ENUM_CASE_STRINGIFY3(Type, TogglePointerMode, withClass); ENUM_CASE_STRINGIFY3(Type, ScrollHorizontal, withClass); ENUM_CASE_STRINGIFY3(Type, ScrollVertical, withClass); ENUM_CASE_STRINGIFY3(Type, VolumeControl, withClass); diff --git a/src/deviceinput.h b/src/deviceinput.h index 3ad55319..3536cd1d 100644 --- a/src/deviceinput.h +++ b/src/deviceinput.h @@ -170,6 +170,8 @@ struct Action KeySequence = 1, CyclePresets = 2, ToggleSpotlight = 3, + ToggleLaser = 4, + TogglePointerMode = 5, ScrollHorizontal = 11, ScrollVertical = 12, VolumeControl = 13, @@ -222,6 +224,28 @@ struct ToggleSpotlightAction : public Action bool placeholder = false; }; +// ------------------------------------------------------------------------------------------------- +struct ToggleLaserAction : public Action +{ + Type type() const override { return Type::ToggleLaser; } + QDataStream& save(QDataStream& s) const override { return s << placeholder; } + QDataStream& load(QDataStream& s) override { return s >> placeholder; } + bool empty() const override { return false; } + bool operator==(const ToggleLaserAction&) const { return true; } + bool placeholder = false; +}; + +// ------------------------------------------------------------------------------------------------- +struct TogglePointerModeAction : public Action +{ + Type type() const override { return Type::TogglePointerMode; } + QDataStream& save(QDataStream& s) const override { return s << placeholder; } + QDataStream& load(QDataStream& s) override { return s >> placeholder; } + bool empty() const override { return false; } + bool operator==(const TogglePointerModeAction&) const { return true; } + bool placeholder = false; +}; + // ------------------------------------------------------------------------------------------------- struct ScrollHorizontalAction : public Action { diff --git a/src/inputmapconfig.cc b/src/inputmapconfig.cc index 537bc694..82d22bac 100644 --- a/src/inputmapconfig.cc +++ b/src/inputmapconfig.cc @@ -220,6 +220,12 @@ void InputMapConfigModel::setItemActionType(const QModelIndex& idx, Action::Type case Action::Type::ToggleSpotlight: item.action = std::make_shared(); break; + case Action::Type::ToggleLaser: + item.action = std::make_shared(); + break; + case Action::Type::TogglePointerMode: + item.action = std::make_shared(); + break; case Action::Type::ScrollHorizontal: item.action = GlobalActions::scrollHorizontal(); break; diff --git a/src/main.cc b/src/main.cc index c26abb37..2f9ac104 100644 --- a/src/main.cc +++ b/src/main.cc @@ -346,15 +346,15 @@ namespace { print() << " -c COMMAND|PROPERTY " << commandOption_.description() << std::endl; print() << ""; print() << " spot=[on|off|toggle] " << i18n("Turn spotlight on/off or toggle."); + print() << " laser=[on|off|toggle] " << i18n("Turn laser pointer on/off or toggle."); + print() << " pointer=[spotlight|laser|toggle] " << i18n("Set or toggle pointer mode."); if (fullHelp) { print() << " preset=NAME " << i18n("Set a preset."); print() << " vibrate[=I[,L]] " << i18n("Send vibrate command to device with intensity,length."); print() << " spot.size.adjust=[+|-]N " << i18n("Increase or decrease spot size by N."); + print() << " laser.size.adjust=[+|-]N " << i18n("Increase or decrease laser size by N."); } print() << " settings=[show|hide] " << i18n("Show/hide preferences dialog."); - if (fullHelp) { - print() << " preset=NAME " << i18n("Set a preset."); - } print() << " quit " << i18n("Quit the running instance."); // Early return if the user not explicitly requested the full help diff --git a/src/org.projecteur.Projecteur.xml b/src/org.projecteur.Projecteur.xml index 3b380bb2..3d2d90f1 100644 --- a/src/org.projecteur.Projecteur.xml +++ b/src/org.projecteur.Projecteur.xml @@ -8,6 +8,9 @@ + + + @@ -44,6 +47,11 @@ + + + + + @@ -70,6 +78,9 @@ + + + diff --git a/src/preferencesdlg.cc b/src/preferencesdlg.cc index 48bd8f40..f3c10f55 100644 --- a/src/preferencesdlg.cc +++ b/src/preferencesdlg.cc @@ -69,13 +69,25 @@ PreferencesDialog::PreferencesDialog(Settings* settings, Spotlight* spotlight, const auto spotlightPage = new QWidget(this); const auto spotlightLayout = new QVBoxLayout(spotlightPage); - const auto overlayCheckBox = new QCheckBox(i18n("Enable spotlight overlay"), spotlightPage); + const auto overlayCheckBox = new QCheckBox(i18n("Enable overlay"), spotlightPage); overlayCheckBox->setChecked(!settings->overlayDisabled()); spotlightLayout->addWidget(overlayCheckBox); spotlightLayout->addWidget(settingsWidget); + const auto laserSettingsWidget = createLaserTabWidget(settings); + laserSettingsWidget->setDisabled(settings->overlayDisabled()); + + const auto laserPage = new QWidget(this); + const auto laserLayout = new QVBoxLayout(laserPage); + const auto laserOverlayCheckBox = new QCheckBox(i18n("Enable overlay"), laserPage); + laserOverlayCheckBox->setChecked(!settings->overlayDisabled()); + laserLayout->addWidget(laserOverlayCheckBox); + laserLayout->addWidget(laserSettingsWidget); + addPage(spotlightPage, i18n("Spotlight"), QStringLiteral("preferences-desktop-display"), QString(), false); + addPage(laserPage, i18n("Laser Pointer"), QStringLiteral("draw-cross-symbolic"), + QString(), false); m_deviceswidget = new DevicesWidget(settings, spotlight, this); addPage(m_deviceswidget, i18n("Devices"), QStringLiteral("input-mouse"), QString(), false); m_shortcutsEditor = new KShortcutsEditor( @@ -88,14 +100,18 @@ PreferencesDialog::PreferencesDialog(Settings* settings, Spotlight* spotlight, helpButton->hide(); } - connect(overlayCheckBox, &QCheckBox::toggled, this, [settings](bool checked){ + const auto toggleOverlay = [settings](bool checked){ settings->setOverlayDisabled(!checked); - }); + }; + connect(overlayCheckBox, &QCheckBox::toggled, this, toggleOverlay); + connect(laserOverlayCheckBox, &QCheckBox::toggled, this, toggleOverlay); connect(settings, &Settings::overlayDisabledChanged, this, - [overlayCheckBox, settingsWidget](bool disabled){ + [overlayCheckBox, laserOverlayCheckBox, settingsWidget, laserSettingsWidget](bool disabled){ overlayCheckBox->setChecked(!disabled); + laserOverlayCheckBox->setChecked(!disabled); settingsWidget->setDisabled(disabled); + laserSettingsWidget->setDisabled(disabled); }); const auto modified = [this]() { settingsModified(); }; @@ -117,6 +133,19 @@ PreferencesDialog::PreferencesDialog(Settings* settings, Spotlight* spotlight, connect(settings, &Settings::zoomEnabledChanged, this, modified); connect(settings, &Settings::zoomFactorChanged, this, modified); connect(settings, &Settings::zoomModeChanged, this, modified); + connect(settings, &Settings::pointerModeChanged, this, modified); + connect(settings, &Settings::laserSizeChanged, this, modified); + connect(settings, &Settings::laserColorChanged, this, modified); + connect(settings, &Settings::laserOpacityChanged, this, modified); + connect(settings, &Settings::laserGlowChanged, this, modified); + connect(settings, &Settings::laserGlowSizeChanged, this, modified); + connect(settings, &Settings::laserGlowOpacityChanged, this, modified); + connect(settings, &Settings::laserGlowColorChanged, this, modified); + connect(settings, &Settings::laserTrailChanged, this, modified); + connect(settings, &Settings::laserTrailTimeChanged, this, modified); + connect(settings, &Settings::laserTrailWidthChanged, this, modified); + connect(settings, &Settings::laserTrailColorChanged, this, modified); + connect(settings, &Settings::laserTrailOpacityChanged, this, modified); connect(settings, &Settings::multiScreenOverlayEnabledChanged, this, modified); for (const auto& shape : Settings::spotShapes()) { if (auto* shapeSettings = settings->shapeSettings(shape.name())) { @@ -135,6 +164,7 @@ QWidget* PreferencesDialog::createSettingsTabWidget(Settings* settings) const auto widget = new QWidget(this); const auto mainHBox = new QHBoxLayout; const auto spotScreenVBoxLeft = new QVBoxLayout(); + spotScreenVBoxLeft->addWidget(createPointerModeGroupBox(settings)); spotScreenVBoxLeft->addWidget(createShapeGroupBox(settings)); spotScreenVBoxLeft->addWidget(createZoomGroupBox(settings)); spotScreenVBoxLeft->addWidget(createCursorGroupBox(settings)); @@ -163,6 +193,238 @@ QWidget* PreferencesDialog::createSettingsTabWidget(Settings* settings) return widget; } +QWidget* PreferencesDialog::createLaserTabWidget(Settings* settings) +{ + const auto widget = new QWidget(this); + const auto mainHBox = new QHBoxLayout; + const auto laserVBoxLeft = new QVBoxLayout(); + laserVBoxLeft->addWidget(createPointerModeGroupBox(settings)); + laserVBoxLeft->addWidget(createLaserDotGroupBox(settings)); + laserVBoxLeft->addWidget(createCursorGroupBox(settings)); + laserVBoxLeft->addWidget(createMultiScreenWidget(settings)); + const auto laserVBoxRight = new QVBoxLayout(); + laserVBoxRight->addWidget(createLaserGlowGroupBox(settings)); + laserVBoxRight->addWidget(createLaserTrailGroupBox(settings)); + mainHBox->addLayout(laserVBoxLeft); + mainHBox->addLayout(laserVBoxRight); + + const auto presetSelector = createPresetSelector(settings); + + const auto testBtn = new QPushButton(i18n("&Show test..."), widget); + connect(testBtn, &QPushButton::clicked, this, &PreferencesDialog::testButtonClicked); + + const auto hbox = new QHBoxLayout; + hbox->addWidget(testBtn); + hbox->addStretch(1); + + const auto mainVBox = new QVBoxLayout(widget); + mainVBox->addLayout(mainHBox); + mainVBox->addWidget(presetSelector); + mainVBox->addLayout(hbox); + + return widget; +} + +QGroupBox* PreferencesDialog::createPointerModeGroupBox(Settings* settings) +{ + const auto modeGroup = new QGroupBox(i18n("Active Pointer Mode"), this); + const auto grid = new QGridLayout(modeGroup); + const auto modeCombo = new QComboBox(this); + modeCombo->addItem(i18n("Spotlight (Screen dimming + shape highlight)"), QStringLiteral("spotlight")); + modeCombo->addItem(i18n("Laser Pointer (Digital laser dot)"), QStringLiteral("laser")); + modeCombo->setCurrentIndex(modeCombo->findData(settings->pointerMode())); + connect(modeCombo, &QComboBox::currentIndexChanged, settings, + [settings, modeCombo](int index) { + settings->setPointerMode(modeCombo->itemData(index).toString()); + }); + connect(settings, &Settings::pointerModeChanged, modeCombo, + [modeCombo](const QString& mode) { + const auto index = modeCombo->findData(mode); + if (index >= 0) { modeCombo->setCurrentIndex(index); } + }); + connect(settings, &Settings::pointerModeChanged, this, &PreferencesDialog::resetPresetCombo); + + grid->addWidget(new QLabel(i18n("Pointer Mode"), this), 0, 0); + grid->addWidget(modeCombo, 0, 1); + grid->setColumnStretch(1, 1); + return modeGroup; +} + +QGroupBox* PreferencesDialog::createLaserDotGroupBox(Settings* settings) +{ + const auto laserGroup = new QGroupBox(i18n("Laser Dot Settings"), this); + const auto grid = new QGridLayout(laserGroup); + + // Laser Size + const auto laserSizeSpinBox = new QSpinBox(this); + laserSizeSpinBox->setMaximum(settings->laserSizeRange().max); + laserSizeSpinBox->setMinimum(settings->laserSizeRange().min); + laserSizeSpinBox->setValue(settings->laserSize()); + const auto lasersizeHBox = new QHBoxLayout; + lasersizeHBox->addWidget(laserSizeSpinBox); + lasersizeHBox->addWidget(new QLabel(i18n("pixel"))); + connect(laserSizeSpinBox, static_cast(&QSpinBox::valueChanged), + settings, &Settings::setLaserSize); + connect(settings, &Settings::laserSizeChanged, laserSizeSpinBox, &QSpinBox::setValue); + connect(settings, &Settings::laserSizeChanged, this, &PreferencesDialog::resetPresetCombo); + grid->addWidget(new QLabel(i18n("Laser Size"), this), 0, 0); + grid->addLayout(lasersizeHBox, 0, 1); + + // Laser Color + const auto laserColor = new KColorButton(settings->laserColor(), this); + laserColor->setAccessibleName(i18n("Laser Color")); + connect(laserColor, &KColorButton::changed, settings, &Settings::setLaserColor); + connect(settings, &Settings::laserColorChanged, laserColor, &KColorButton::setColor); + connect(settings, &Settings::laserColorChanged, this, &PreferencesDialog::resetPresetCombo); + grid->addWidget(new QLabel(i18n("Laser Color"), this), 1, 0); + grid->addWidget(laserColor, 1, 1); + + // Laser Opacity + const auto laserOpacitySb = new QDoubleSpinBox(this); + laserOpacitySb->setMaximum(settings->laserOpacityRange().max); + laserOpacitySb->setMinimum(settings->laserOpacityRange().min); + laserOpacitySb->setDecimals(2); + laserOpacitySb->setSingleStep(0.05); + laserOpacitySb->setValue(settings->laserOpacity()); + connect(laserOpacitySb, static_cast(&QDoubleSpinBox::valueChanged), + settings, &Settings::setLaserOpacity); + connect(settings, &Settings::laserOpacityChanged, laserOpacitySb, &QDoubleSpinBox::setValue); + connect(settings, &Settings::laserOpacityChanged, this, &PreferencesDialog::resetPresetCombo); + grid->addWidget(new QLabel(i18n("Laser Opacity"), this), 2, 0); + grid->addWidget(laserOpacitySb, 2, 1); + + grid->addWidget(new QWidget(this), 100, 0); + grid->setRowStretch(100, 100); + grid->setColumnStretch(1, 1); + return laserGroup; +} + +QGroupBox* PreferencesDialog::createLaserGlowGroupBox(Settings* settings) +{ + const auto glowGroup = new QGroupBox(i18n("Laser Glow / Halo"), this); + glowGroup->setCheckable(true); + glowGroup->setChecked(settings->laserGlow()); + connect(glowGroup, &QGroupBox::toggled, settings, &Settings::setLaserGlow); + connect(settings, &Settings::laserGlowChanged, glowGroup, &QGroupBox::setChecked); + connect(settings, &Settings::laserGlowChanged, this, &PreferencesDialog::resetPresetCombo); + + const auto grid = new QGridLayout(glowGroup); + + // Glow Size + const auto glowSizeSpinBox = new QSpinBox(this); + glowSizeSpinBox->setMaximum(settings->laserGlowSizeRange().max); + glowSizeSpinBox->setMinimum(settings->laserGlowSizeRange().min); + glowSizeSpinBox->setValue(settings->laserGlowSize()); + const auto glowsizeHBox = new QHBoxLayout; + glowsizeHBox->addWidget(glowSizeSpinBox); + glowsizeHBox->addWidget(new QLabel(i18n("pixel"))); + connect(glowSizeSpinBox, static_cast(&QSpinBox::valueChanged), + settings, &Settings::setLaserGlowSize); + connect(settings, &Settings::laserGlowSizeChanged, glowSizeSpinBox, &QSpinBox::setValue); + connect(settings, &Settings::laserGlowSizeChanged, this, &PreferencesDialog::resetPresetCombo); + grid->addWidget(new QLabel(i18n("Glow Size"), this), 0, 0); + grid->addLayout(glowsizeHBox, 0, 1); + + // Glow Color + const auto glowColor = new KColorButton(settings->laserGlowColor(), this); + glowColor->setAccessibleName(i18n("Glow Color")); + connect(glowColor, &KColorButton::changed, settings, &Settings::setLaserGlowColor); + connect(settings, &Settings::laserGlowColorChanged, glowColor, &KColorButton::setColor); + connect(settings, &Settings::laserGlowColorChanged, this, &PreferencesDialog::resetPresetCombo); + grid->addWidget(new QLabel(i18n("Glow Color"), this), 1, 0); + grid->addWidget(glowColor, 1, 1); + + // Glow Opacity + const auto glowOpacitySb = new QDoubleSpinBox(this); + glowOpacitySb->setMaximum(settings->laserGlowOpacityRange().max); + glowOpacitySb->setMinimum(settings->laserGlowOpacityRange().min); + glowOpacitySb->setDecimals(2); + glowOpacitySb->setSingleStep(0.05); + glowOpacitySb->setValue(settings->laserGlowOpacity()); + connect(glowOpacitySb, static_cast(&QDoubleSpinBox::valueChanged), + settings, &Settings::setLaserGlowOpacity); + connect(settings, &Settings::laserGlowOpacityChanged, glowOpacitySb, &QDoubleSpinBox::setValue); + connect(settings, &Settings::laserGlowOpacityChanged, this, &PreferencesDialog::resetPresetCombo); + grid->addWidget(new QLabel(i18n("Glow Opacity"), this), 2, 0); + grid->addWidget(glowOpacitySb, 2, 1); + + grid->addWidget(new QWidget(this), 100, 0); + grid->setRowStretch(100, 100); + grid->setColumnStretch(1, 1); + return glowGroup; +} + +QGroupBox* PreferencesDialog::createLaserTrailGroupBox(Settings* settings) +{ + const auto trailGroup = new QGroupBox(i18n("Laser Trail Mark"), this); + trailGroup->setCheckable(true); + trailGroup->setChecked(settings->laserTrail()); + connect(trailGroup, &QGroupBox::toggled, settings, &Settings::setLaserTrail); + connect(settings, &Settings::laserTrailChanged, trailGroup, &QGroupBox::setChecked); + connect(settings, &Settings::laserTrailChanged, this, &PreferencesDialog::resetPresetCombo); + + const auto grid = new QGridLayout(trailGroup); + + // Trail Duration + const auto trailTimeSpinBox = new QSpinBox(this); + trailTimeSpinBox->setMaximum(settings->laserTrailTimeRange().max); + trailTimeSpinBox->setMinimum(settings->laserTrailTimeRange().min); + trailTimeSpinBox->setSingleStep(50); + trailTimeSpinBox->setValue(settings->laserTrailTime()); + const auto timeHBox = new QHBoxLayout; + timeHBox->addWidget(trailTimeSpinBox); + timeHBox->addWidget(new QLabel(i18n("ms"))); + connect(trailTimeSpinBox, static_cast(&QSpinBox::valueChanged), + settings, &Settings::setLaserTrailTime); + connect(settings, &Settings::laserTrailTimeChanged, trailTimeSpinBox, &QSpinBox::setValue); + connect(settings, &Settings::laserTrailTimeChanged, this, &PreferencesDialog::resetPresetCombo); + grid->addWidget(new QLabel(i18n("Trail Duration"), this), 0, 0); + grid->addLayout(timeHBox, 0, 1); + + // Trail Width + const auto trailWidthSpinBox = new QSpinBox(this); + trailWidthSpinBox->setMaximum(settings->laserTrailWidthRange().max); + trailWidthSpinBox->setMinimum(settings->laserTrailWidthRange().min); + trailWidthSpinBox->setValue(settings->laserTrailWidth()); + const auto widthHBox = new QHBoxLayout; + widthHBox->addWidget(trailWidthSpinBox); + widthHBox->addWidget(new QLabel(i18n("pixel"))); + connect(trailWidthSpinBox, static_cast(&QSpinBox::valueChanged), + settings, &Settings::setLaserTrailWidth); + connect(settings, &Settings::laserTrailWidthChanged, trailWidthSpinBox, &QSpinBox::setValue); + connect(settings, &Settings::laserTrailWidthChanged, this, &PreferencesDialog::resetPresetCombo); + grid->addWidget(new QLabel(i18n("Trail Width"), this), 1, 0); + grid->addLayout(widthHBox, 1, 1); + + // Trail Color + const auto trailColor = new KColorButton(settings->laserTrailColor(), this); + trailColor->setAccessibleName(i18n("Trail Color")); + connect(trailColor, &KColorButton::changed, settings, &Settings::setLaserTrailColor); + connect(settings, &Settings::laserTrailColorChanged, trailColor, &KColorButton::setColor); + connect(settings, &Settings::laserTrailColorChanged, this, &PreferencesDialog::resetPresetCombo); + grid->addWidget(new QLabel(i18n("Trail Color"), this), 2, 0); + grid->addWidget(trailColor, 2, 1); + + // Trail Opacity + const auto trailOpacitySb = new QDoubleSpinBox(this); + trailOpacitySb->setMaximum(settings->laserTrailOpacityRange().max); + trailOpacitySb->setMinimum(settings->laserTrailOpacityRange().min); + trailOpacitySb->setDecimals(2); + trailOpacitySb->setSingleStep(0.05); + trailOpacitySb->setValue(settings->laserTrailOpacity()); + connect(trailOpacitySb, static_cast(&QDoubleSpinBox::valueChanged), + settings, &Settings::setLaserTrailOpacity); + connect(settings, &Settings::laserTrailOpacityChanged, trailOpacitySb, &QDoubleSpinBox::setValue); + connect(settings, &Settings::laserTrailOpacityChanged, this, &PreferencesDialog::resetPresetCombo); + grid->addWidget(new QLabel(i18n("Trail Opacity"), this), 3, 0); + grid->addWidget(trailOpacitySb, 3, 1); + + grid->addWidget(new QWidget(this), 100, 0); + grid->setRowStretch(100, 100); + grid->setColumnStretch(1, 1); + return trailGroup; +} + // ------------------------------------------------------------------------------------------------- QWidget* PreferencesDialog::createPresetSelector(Settings* settings) { @@ -171,20 +433,22 @@ QWidget* PreferencesDialog::createPresetSelector(Settings* settings) const auto hbox = new QHBoxLayout(widget); hbox->addWidget(new QLabel(i18n("Presets"), widget)); - m_presetCombo = new QComboBox(widget); - m_presetCombo->setModel(settings->presetModel()); + auto* const presetCombo = new QComboBox(widget); + presetCombo->setModel(settings->presetModel()); + m_presetCombos.push_back(presetCombo); + m_presetCombo = presetCombo; - const auto normalComboStyle = m_presetCombo->style(); - m_presetCombo->setStyle(&*m_presetComboStyle); // style when no preset is selected - m_presetCombo->setInsertPolicy(QComboBox::NoInsert); + const auto normalComboStyle = presetCombo->style(); + presetCombo->setStyle(&*m_presetComboStyle); // style when no preset is selected + presetCombo->setInsertPolicy(QComboBox::NoInsert); const auto deleteBtn = new IconButton(Font::Icon::trash_can_1, widget); deleteBtn->setToolTip(i18n("Delete currently selected preset.")); - deleteBtn->setEnabled(m_presetCombo->currentIndex() > 0); + deleteBtn->setEnabled(presetCombo->currentIndex() > 0); const auto newBtn = new IconButton(Font::Icon::plus_5, widget); - newBtn->setToolTip(i18n("Create new preset from current spotlight settings.")); + newBtn->setToolTip(i18n("Create new preset from current settings.")); - const std::vector widgets{m_presetCombo, deleteBtn, newBtn}; + const std::vector widgets{presetCombo, deleteBtn, newBtn}; for (const auto w : widgets) { w->setSizePolicy(w->sizePolicy().horizontalPolicy(), QSizePolicy::Minimum); hbox->addWidget(w); @@ -192,38 +456,38 @@ QWidget* PreferencesDialog::createPresetSelector(Settings* settings) hbox->setStretch(1, 1); // stretch combobox - connect(m_presetCombo, static_cast(&QComboBox::currentIndexChanged), widget, - [deleteBtn, settings, normalComboStyle, this](int index) + connect(presetCombo, static_cast(&QComboBox::currentIndexChanged), widget, + [presetCombo, deleteBtn, settings, normalComboStyle, this](int index) { deleteBtn->setEnabled(index > 0); - m_presetCombo->setStyle(index == 0 ? &*m_presetComboStyle : normalComboStyle); + presetCombo->setStyle(index == 0 ? &*m_presetComboStyle : normalComboStyle); - if (index > 0 && !m_presetCombo->currentText().isEmpty()) { - settings->loadPreset(m_presetCombo->currentText()); + if (index > 0 && !presetCombo->currentText().isEmpty()) { + settings->loadPreset(presetCombo->currentText()); } }); - connect(newBtn, &QPushButton::clicked, this, [newBtn, settings, this]() + connect(newBtn, &QPushButton::clicked, this, [newBtn, presetCombo, settings, this]() { newBtn->setEnabled(false); - m_presetCombo->setEditable(true); - const auto le = m_presetCombo->lineEdit(); + presetCombo->setEditable(true); + const auto le = presetCombo->lineEdit(); le->setMaxLength(35); le->setCompleter(nullptr); - connect(le, &QLineEdit::editingFinished, this, [le, settings, newBtn, this]() + connect(le, &QLineEdit::editingFinished, this, [le, presetCombo, settings, newBtn, this]() { auto text = le->text().trimmed(); - m_presetCombo->setEditable(false); + presetCombo->setEditable(false); if (text.isEmpty()) { - text = m_presetCombo->currentText().trimmed(); + text = presetCombo->currentText().trimmed(); } - if (m_presetCombo->findText(text) >= 0) { // Item with same name already exists + if (presetCombo->findText(text) >= 0) { // Item with same name already exists text.append(" (%1)"); for (int i = 2; i < 1000; ++i) { - if (m_presetCombo->findText(text.arg(i)) < 0) { + if (presetCombo->findText(text.arg(i)) < 0) { text = text.arg(i); break; } @@ -239,24 +503,27 @@ QWidget* PreferencesDialog::createPresetSelector(Settings* settings) le->selectAll(); }); - connect(deleteBtn, &QPushButton::clicked, this, [this, settings]() + connect(deleteBtn, &QPushButton::clicked, this, [presetCombo, settings]() { - if (m_presetCombo->currentIndex() < 0) { return; } - settings->removePreset(m_presetCombo->currentText()); + if (presetCombo->currentIndex() < 0) { return; } + settings->removePreset(presetCombo->currentText()); }); connect(settings, &Settings::presetLoaded, this, [normalComboStyle, deleteBtn, this](const QString& preset) { - const auto idx = m_presetCombo->findText(preset); - if (idx >= 0 && idx != m_presetCombo->currentIndex()) - { - m_presetCombo->blockSignals(true); - m_presetCombo->setCurrentIndex(idx); - m_presetCombo->blockSignals(false); - m_presetCombo->setStyle(idx == 0 ? &*m_presetComboStyle : normalComboStyle); - deleteBtn->setEnabled(idx > 0); + for (auto* combo : m_presetCombos) { + if (!combo) continue; + const auto idx = combo->findText(preset); + if (idx >= 0 && idx != combo->currentIndex()) + { + combo->blockSignals(true); + combo->setCurrentIndex(idx); + combo->blockSignals(false); + combo->setStyle(idx == 0 ? &*m_presetComboStyle : normalComboStyle); + } } + deleteBtn->setEnabled(m_presetCombos.empty() ? false : m_presetCombos.front()->currentIndex() > 0); }); return widget; @@ -486,8 +753,8 @@ QGroupBox* PreferencesDialog::createDotGroupBox(Settings* settings) dotOpacitySb->setValue(settings->dotOpacity()); connect(dotOpacitySb, static_cast(&QDoubleSpinBox::valueChanged), settings, &Settings::setDotOpacity); - connect(settings, &Settings::borderOpacityChanged, dotOpacitySb, &QDoubleSpinBox::setValue); - connect(settings, &Settings::borderOpacityChanged, this, &PreferencesDialog::resetPresetCombo); + connect(settings, &Settings::dotOpacityChanged, dotOpacitySb, &QDoubleSpinBox::setValue); + connect(settings, &Settings::dotOpacityChanged, this, &PreferencesDialog::resetPresetCombo); dotGrid->addWidget(new QLabel(i18n("Dot Opacity"), this), 2, 0); dotGrid->addWidget(dotOpacitySb, 2, 1); @@ -766,7 +1033,9 @@ void PreferencesDialog::reject() // ------------------------------------------------------------------------------------------------- void PreferencesDialog::resetPresetCombo() { - if (m_presetCombo) { m_presetCombo->setCurrentIndex(0); } + for (auto* combo : m_presetCombos) { + if (combo) { combo->setCurrentIndex(0); } + } } // ------------------------------------------------------------------------------------------------- diff --git a/src/preferencesdlg.h b/src/preferencesdlg.h index 7a01e0fd..8bbecfd1 100644 --- a/src/preferencesdlg.h +++ b/src/preferencesdlg.h @@ -76,6 +76,11 @@ protected slots: void resetPresetCombo(); QWidget* createSettingsTabWidget(Settings* settings); + QWidget* createLaserTabWidget(Settings* settings); + QGroupBox* createPointerModeGroupBox(Settings* settings); + QGroupBox* createLaserDotGroupBox(Settings* settings); + QGroupBox* createLaserGlowGroupBox(Settings* settings); + QGroupBox* createLaserTrailGroupBox(Settings* settings); QGroupBox* createShapeGroupBox(Settings* settings); QGroupBox* createSpotGroupBox(Settings* settings); QGroupBox* createDotGroupBox(Settings* settings); @@ -91,6 +96,7 @@ protected slots: QVariantMap m_appliedSpotlightSettings; std::unique_ptr m_presetComboStyle; QComboBox* m_presetCombo = nullptr; + std::vector m_presetCombos; DevicesWidget* m_deviceswidget = nullptr; KShortcutsEditor* m_shortcutsEditor = nullptr; bool m_active = false; diff --git a/src/projecteurapp.cc b/src/projecteurapp.cc index 3dcf7cba..87a5111b 100644 --- a/src/projecteurapp.cc +++ b/src/projecteurapp.cc @@ -311,6 +311,14 @@ void ProjecteurApplication::setupGlobalShortcuts() m_control->SetSpotlightActive(!m_control->spotlightActive()); } }); + addAction( + QStringLiteral("toggle_laser"), i18n("Toggle Laser Pointer"), + QStringLiteral("draw-cross-symbolic"), + [this]() { m_control->ToggleLaserActive(); }); + addAction( + QStringLiteral("toggle_pointer_mode"), i18n("Toggle Pointer Mode"), + QStringLiteral("view-refresh"), + [this]() { m_control->TogglePointerMode(); }); addAction( QStringLiteral("show_preferences"), i18n("Show Preferences"), QStringLiteral("configure"), @@ -710,6 +718,17 @@ void ProjecteurApplication::applyCommand(const QString& command) qCDebug(PROJECTEUR_COMMAND_LOG).noquote() << QStringLiteral("Received invalid value for command spot.size.adjust"); } } + else if (cmdKey == "laser.size.adjust") + { + bool ok = false; + int const sizeAdjust = cmdValue.toInt(&ok); + if (ok) { + qCDebug(PROJECTEUR_COMMAND_LOG).noquote() << QStringLiteral("Received command laser.size.adjust = %1%2").arg(sizeAdjust > 0 ? "+" : "").arg(sizeAdjust); + m_settings->setLaserSize(m_settings->laserSize() + sizeAdjust); + } else { + qCDebug(PROJECTEUR_COMMAND_LOG).noquote() << QStringLiteral("Received invalid value for command laser.size.adjust"); + } + } else if (cmdKey == "spot") { if (cmdValue.isEmpty()) { @@ -725,6 +744,37 @@ void ProjecteurApplication::applyCommand(const QString& command) m_spotlight->setSpotActive(active); } } + else if (cmdKey == "laser") + { + if (cmdValue.isEmpty()) { + qCDebug(PROJECTEUR_COMMAND_LOG).noquote() << QStringLiteral("Received empty command value for command laser"); + } else if (cmdValue.toLower() == "toggle") { + m_control->ToggleLaserActive(); + } else { + const bool active = (cmdValue.toLower() == "on" + || cmdValue == "1" + || cmdValue.toLower() == "true"); + qCDebug(PROJECTEUR_COMMAND_LOG).noquote() << QStringLiteral("Received command laser = %1").arg(active); + if (active) { + if (m_settings->pointerMode() != QStringLiteral("laser")) { + m_settings->setPointerMode(QStringLiteral("laser")); + } + m_settings->setOverlayDisabled(false); + } else { + m_settings->setOverlayDisabled(true); + } + } + } + else if (cmdKey == "pointer") + { + if (cmdValue.toLower() == "toggle") { + m_control->TogglePointerMode(); + } else if (Settings::isPointerMode(cmdValue.toLower())) { + m_settings->setPointerMode(cmdValue.toLower()); + } else { + qCDebug(PROJECTEUR_COMMAND_LOG).noquote() << QStringLiteral("Received invalid value for command pointer: %1").arg(cmdValue); + } + } else if (cmdKey == "settings" || cmdKey == "preferences") { const bool show = !(cmdValue.toLower() == "hide" || cmdValue == "0"); diff --git a/src/projecteurconfig.kcfg b/src/projecteurconfig.kcfg index 008430ab..1ab6902c 100644 --- a/src/projecteurconfig.kcfg +++ b/src/projecteurconfig.kcfg @@ -75,6 +75,59 @@ smooth + + spotlight + + + 16 + 3 + 100 + + + #ff0000 + + + 0.9 + 0.0 + 1.0 + + + true + + + 10 + 0 + 50 + + + 0.6 + 0.0 + 1.0 + + + #ff0000 + + + false + + + 400 + 100 + 2000 + + + 4 + 1 + 30 + + + #ff0000 + + + 0.8 + 0.0 + 1.0 + false diff --git a/src/projecteurcontrol.cc b/src/projecteurcontrol.cc index eb540f75..59328c56 100644 --- a/src/projecteurcontrol.cc +++ b/src/projecteurcontrol.cc @@ -58,6 +58,10 @@ ProjecteurControl::ProjecteurControl(ProjecteurApplication* application, Setting emit spotlightActiveChanged(active); emitPropertiesChanged({{QStringLiteral("SpotlightActive"), active}}); }); + connect(m_settings, &Settings::pointerModeChanged, this, [this](const QString& mode) { + emit pointerModeChanged(mode); + emitPropertiesChanged({{QStringLiteral("PointerMode"), mode}}); + }); connect(m_presentationTimer, &PresentationTimer::enabledChanged, this, [this](bool enabled) { emit timerEnabledChanged(enabled); @@ -138,6 +142,14 @@ ProjecteurControl::ProjecteurControl(ProjecteurApplication* application, Setting connect(m_settings, &Settings::zoomEnabledChanged, this, settingsChanged); connect(m_settings, &Settings::zoomFactorChanged, this, settingsChanged); connect(m_settings, &Settings::zoomModeChanged, this, settingsChanged); + connect(m_settings, &Settings::pointerModeChanged, this, settingsChanged); + connect(m_settings, &Settings::laserSizeChanged, this, settingsChanged); + connect(m_settings, &Settings::laserColorChanged, this, settingsChanged); + connect(m_settings, &Settings::laserOpacityChanged, this, settingsChanged); + connect(m_settings, &Settings::laserGlowChanged, this, settingsChanged); + connect(m_settings, &Settings::laserGlowSizeChanged, this, settingsChanged); + connect(m_settings, &Settings::laserGlowOpacityChanged, this, settingsChanged); + connect(m_settings, &Settings::laserGlowColorChanged, this, settingsChanged); connect(m_settings, &Settings::multiScreenOverlayEnabledChanged, this, settingsChanged); for (const auto& shape : Settings::spotShapes()) { @@ -182,6 +194,11 @@ bool ProjecteurControl::spotlightActive() const return m_spotlight->spotActive(); } +QString ProjecteurControl::pointerMode() const +{ + return m_settings->pointerMode(); +} + QStringList ProjecteurControl::connectedDevices() const { QStringList result; @@ -279,6 +296,32 @@ void ProjecteurControl::SetSpotlightActive(bool active) m_spotlight->setSpotActive(active); } +void ProjecteurControl::SetPointerMode(const QString& mode) +{ + if (Settings::isPointerMode(mode)) { + m_settings->setPointerMode(mode); + } +} + +void ProjecteurControl::TogglePointerMode() +{ + m_settings->setPointerMode(m_settings->pointerMode() == QStringLiteral("laser") + ? QStringLiteral("spotlight") + : QStringLiteral("laser")); +} + +void ProjecteurControl::ToggleLaserActive() +{ + if (m_settings->pointerMode() != QStringLiteral("laser")) { + m_settings->setPointerMode(QStringLiteral("laser")); + if (m_settings->overlayDisabled()) { + m_settings->setOverlayDisabled(false); + } + } else { + m_settings->setOverlayDisabled(!m_settings->overlayDisabled()); + } +} + bool ProjecteurControl::LoadPreset(const QString& preset) { if (!m_settings->presetModel()->hasPreset(preset)) { return false; } diff --git a/src/projecteurcontrol.h b/src/projecteurcontrol.h index e130b447..5b2e2adc 100644 --- a/src/projecteurcontrol.h +++ b/src/projecteurcontrol.h @@ -22,6 +22,7 @@ class ProjecteurControl : public QObject Q_PROPERTY(bool TrayVisible READ trayVisible CONSTANT) Q_PROPERTY(bool OverlayEnabled READ overlayEnabled NOTIFY overlayEnabledChanged) Q_PROPERTY(bool SpotlightActive READ spotlightActive NOTIFY spotlightActiveChanged) + Q_PROPERTY(QString PointerMode READ pointerMode WRITE SetPointerMode NOTIFY pointerModeChanged) Q_PROPERTY(QStringList ConnectedDevices READ connectedDevices NOTIFY connectedDevicesChanged) Q_PROPERTY(QList ConnectedDeviceBatteryLevels READ connectedDeviceBatteryLevels NOTIFY connectedDeviceBatteryLevelsChanged) @@ -48,6 +49,7 @@ class ProjecteurControl : public QObject bool trayVisible() const { return m_trayVisible; } bool overlayEnabled() const; bool spotlightActive() const; + QString pointerMode() const; QStringList connectedDevices() const; QList connectedDeviceBatteryLevels() const; QStringList connectedDeviceBatteryStatuses() const; @@ -63,6 +65,9 @@ class ProjecteurControl : public QObject public slots: void SetOverlayEnabled(bool enabled); void SetSpotlightActive(bool active); + void SetPointerMode(const QString& mode); + void TogglePointerMode(); + void ToggleLaserActive(); bool LoadPreset(const QString& preset); void SetTimerEnabled(bool enabled); void StartTimer(); @@ -77,6 +82,7 @@ public slots: signals: void overlayEnabledChanged(bool enabled); void spotlightActiveChanged(bool active); + void pointerModeChanged(const QString& mode); void connectedDevicesChanged(const QStringList& devices); void connectedDeviceBatteryLevelsChanged(const QList& levels); void connectedDeviceBatteryStatusesChanged(const QStringList& statuses); diff --git a/src/settings.cc b/src/settings.cc index e1f746cd..72c83e08 100644 --- a/src/settings.cc +++ b/src/settings.cc @@ -51,6 +51,19 @@ namespace { constexpr char zoomEnabled[] = "enableZoom"; constexpr char zoomFactor[] = "zoomFactor"; constexpr char zoomMode[] = "zoomMode"; + constexpr char pointerMode[] = "pointerMode"; + constexpr char laserSize[] = "laserSize"; + constexpr char laserColor[] = "laserColor"; + constexpr char laserOpacity[] = "laserOpacity"; + constexpr char laserGlow[] = "laserGlow"; + constexpr char laserGlowSize[] = "laserGlowSize"; + constexpr char laserGlowOpacity[] = "laserGlowOpacity"; + constexpr char laserGlowColor[] = "laserGlowColor"; + constexpr char laserTrail[] = "laserTrail"; + constexpr char laserTrailTime[] = "laserTrailTime"; + constexpr char laserTrailWidth[] = "laserTrailWidth"; + constexpr char laserTrailColor[] = "laserTrailColor"; + constexpr char laserTrailOpacity[] = "laserTrailOpacity"; constexpr char multiScreenOverlay[] = "multiScreenOverlay"; constexpr char presentationTimerEnabled[] = "presentationTimerEnabled"; constexpr char presentationTimerDurationSeconds[] = "presentationTimerDurationSeconds"; @@ -78,6 +91,19 @@ namespace { constexpr bool zoomEnabled = false; constexpr double zoomFactor = 2.0; constexpr char zoomMode[] = "smooth"; + constexpr char pointerMode[] = "spotlight"; + constexpr int laserSize = 16; + constexpr auto laserColor = Qt::red; + constexpr double laserOpacity = 0.9; + constexpr bool laserGlow = true; + constexpr int laserGlowSize = 10; + constexpr double laserGlowOpacity = 0.6; + constexpr auto laserGlowColor = Qt::red; + constexpr bool laserTrail = false; + constexpr int laserTrailTime = 400; + constexpr int laserTrailWidth = 4; + constexpr auto laserTrailColor = Qt::red; + constexpr double laserTrailOpacity = 0.8; constexpr bool multiScreenOverlay = false; constexpr bool presentationTimerEnabled = false; constexpr int presentationTimerDurationSeconds = 15 * 60; @@ -96,6 +122,13 @@ namespace { constexpr Settings::SettingRange borderSize{ 0, 100 }; constexpr Settings::SettingRange borderOpacity{ 0.0, 1.0 }; constexpr Settings::SettingRange zoomFactor{ 1.5, 20.0 }; + constexpr Settings::SettingRange laserSize{ 3, 100 }; + constexpr Settings::SettingRange laserOpacity{ 0.0, 1.0 }; + constexpr Settings::SettingRange laserGlowSize{ 0, 50 }; + constexpr Settings::SettingRange laserGlowOpacity{ 0.0, 1.0 }; + constexpr Settings::SettingRange laserTrailTime{ 100, 2000 }; + constexpr Settings::SettingRange laserTrailWidth{ 1, 30 }; + constexpr Settings::SettingRange laserTrailOpacity{ 0.0, 1.0 }; constexpr Settings::SettingRange inputSequenceInterval{ 100, 950 }; } // end namespace ranges @@ -368,6 +401,42 @@ void Settings::initializeStringProperties() map.emplace_back( "zoom.mode", StringProperty{ StringProperty::StringEnum, {QStringLiteral("smooth"), QStringLiteral("text"), QStringLiteral("pixel")}, [this](const QString& value){ setZoomMode(value); } } ); + // --- pointer mode + map.emplace_back( "pointer.mode", StringProperty{ StringProperty::StringEnum, + {QStringLiteral("spotlight"), QStringLiteral("laser")}, + [this](const QString& value){ setPointerMode(value); } } ); + // --- laser pointer + map.emplace_back( "laser.size", StringProperty{ StringProperty::Integer, + {::settings::ranges::laserSize.min, ::settings::ranges::laserSize.max}, + [this](const QString& value){ setLaserSize(value.toInt()); } } ); + map.emplace_back( "laser.color", StringProperty{ StringProperty::Color, {}, + [this](const QString& value){ setLaserColor(QColor(value)); } } ); + map.emplace_back( "laser.opacity", StringProperty{ StringProperty::Double, + {::settings::ranges::laserOpacity.min, ::settings::ranges::laserOpacity.max}, + [this](const QString& value){ setLaserOpacity(value.toDouble()); } } ); + map.emplace_back( "laser.glow", StringProperty{ StringProperty::Bool, {false, true}, + [this](const QString& value){ setLaserGlow(toBool(value)); } } ); + map.emplace_back( "laser.glow-size", StringProperty{ StringProperty::Integer, + {::settings::ranges::laserGlowSize.min, ::settings::ranges::laserGlowSize.max}, + [this](const QString& value){ setLaserGlowSize(value.toInt()); } } ); + map.emplace_back( "laser.glow-opacity", StringProperty{ StringProperty::Double, + {::settings::ranges::laserGlowOpacity.min, ::settings::ranges::laserGlowOpacity.max}, + [this](const QString& value){ setLaserGlowOpacity(value.toDouble()); } } ); + map.emplace_back( "laser.glow-color", StringProperty{ StringProperty::Color, {}, + [this](const QString& value){ setLaserGlowColor(QColor(value)); } } ); + map.emplace_back( "laser.trail", StringProperty{ StringProperty::Bool, {false, true}, + [this](const QString& value){ setLaserTrail(toBool(value)); } } ); + map.emplace_back( "laser.trail.time", StringProperty{ StringProperty::Integer, + {::settings::ranges::laserTrailTime.min, ::settings::ranges::laserTrailTime.max}, + [this](const QString& value){ setLaserTrailTime(value.toInt()); } } ); + map.emplace_back( "laser.trail.width", StringProperty{ StringProperty::Integer, + {::settings::ranges::laserTrailWidth.min, ::settings::ranges::laserTrailWidth.max}, + [this](const QString& value){ setLaserTrailWidth(value.toInt()); } } ); + map.emplace_back( "laser.trail.color", StringProperty{ StringProperty::Color, {}, + [this](const QString& value){ setLaserTrailColor(QColor(value)); } } ); + map.emplace_back( "laser.trail.opacity", StringProperty{ StringProperty::Double, + {::settings::ranges::laserTrailOpacity.min, ::settings::ranges::laserTrailOpacity.max}, + [this](const QString& value){ setLaserTrailOpacity(value.toDouble()); } } ); } // ------------------------------------------------------------------------------------------------- @@ -385,6 +454,13 @@ const Settings::SettingRange& Settings::spotRotationRange() { return ::s const Settings::SettingRange& Settings::borderSizeRange() { return settings::ranges::borderSize; } const Settings::SettingRange& Settings::borderOpacityRange() { return settings::ranges::borderOpacity; } const Settings::SettingRange& Settings::zoomFactorRange() { return settings::ranges::zoomFactor; } +const Settings::SettingRange& Settings::laserSizeRange() { return ::settings::ranges::laserSize; } +const Settings::SettingRange& Settings::laserOpacityRange() { return ::settings::ranges::laserOpacity; } +const Settings::SettingRange& Settings::laserGlowSizeRange() { return ::settings::ranges::laserGlowSize; } +const Settings::SettingRange& Settings::laserGlowOpacityRange() { return ::settings::ranges::laserGlowOpacity; } +const Settings::SettingRange& Settings::laserTrailTimeRange() { return ::settings::ranges::laserTrailTime; } +const Settings::SettingRange& Settings::laserTrailWidthRange() { return ::settings::ranges::laserTrailWidth; } +const Settings::SettingRange& Settings::laserTrailOpacityRange() { return ::settings::ranges::laserTrailOpacity; } const Settings::SettingRange& Settings::inputSequenceIntervalRange() { return settings::ranges::inputSequenceInterval; } // ------------------------------------------------------------------------------------------------- @@ -424,6 +500,19 @@ Settings::SpotlightSettings Settings::spotlightSettings() const {::settings::zoomEnabled, m_zoomEnabled}, {::settings::zoomFactor, m_zoomFactor}, {::settings::zoomMode, m_zoomMode}, + {::settings::pointerMode, m_pointerMode}, + {::settings::laserSize, m_laserSize}, + {::settings::laserColor, m_laserColor}, + {::settings::laserOpacity, m_laserOpacity}, + {::settings::laserGlow, m_laserGlow}, + {::settings::laserGlowSize, m_laserGlowSize}, + {::settings::laserGlowOpacity, m_laserGlowOpacity}, + {::settings::laserGlowColor, m_laserGlowColor}, + {::settings::laserTrail, m_laserTrail}, + {::settings::laserTrailTime, m_laserTrailTime}, + {::settings::laserTrailWidth, m_laserTrailWidth}, + {::settings::laserTrailColor, m_laserTrailColor}, + {::settings::laserTrailOpacity, m_laserTrailOpacity}, {::settings::multiScreenOverlay, m_multiScreenOverlayEnabled}, }; @@ -462,6 +551,19 @@ Settings::SpotlightSettings Settings::defaultSpotlightSettings() {::settings::zoomEnabled, ::settings::defaultValue::zoomEnabled}, {::settings::zoomFactor, ::settings::defaultValue::zoomFactor}, {::settings::zoomMode, QString(::settings::defaultValue::zoomMode)}, + {::settings::pointerMode, QString(::settings::defaultValue::pointerMode)}, + {::settings::laserSize, ::settings::defaultValue::laserSize}, + {::settings::laserColor, QColor(::settings::defaultValue::laserColor)}, + {::settings::laserOpacity, ::settings::defaultValue::laserOpacity}, + {::settings::laserGlow, ::settings::defaultValue::laserGlow}, + {::settings::laserGlowSize, ::settings::defaultValue::laserGlowSize}, + {::settings::laserGlowOpacity, ::settings::defaultValue::laserGlowOpacity}, + {::settings::laserGlowColor, QColor(::settings::defaultValue::laserGlowColor)}, + {::settings::laserTrail, ::settings::defaultValue::laserTrail}, + {::settings::laserTrailTime, ::settings::defaultValue::laserTrailTime}, + {::settings::laserTrailWidth, ::settings::defaultValue::laserTrailWidth}, + {::settings::laserTrailColor, QColor(::settings::defaultValue::laserTrailColor)}, + {::settings::laserTrailOpacity, ::settings::defaultValue::laserTrailOpacity}, {::settings::multiScreenOverlay, ::settings::defaultValue::multiScreenOverlay}, }; @@ -496,6 +598,19 @@ void Settings::setSpotlightSettings(const SpotlightSettings& values) setZoomEnabled(values.value(::settings::zoomEnabled, m_zoomEnabled).toBool()); setZoomFactor(values.value(::settings::zoomFactor, m_zoomFactor).toDouble()); setZoomMode(values.value(::settings::zoomMode, m_zoomMode).toString()); + setPointerMode(values.value(::settings::pointerMode, m_pointerMode).toString()); + setLaserSize(values.value(::settings::laserSize, m_laserSize).toInt()); + setLaserColor(values.value(::settings::laserColor, m_laserColor).value()); + setLaserOpacity(values.value(::settings::laserOpacity, m_laserOpacity).toDouble()); + setLaserGlow(values.value(::settings::laserGlow, m_laserGlow).toBool()); + setLaserGlowSize(values.value(::settings::laserGlowSize, m_laserGlowSize).toInt()); + setLaserGlowOpacity(values.value(::settings::laserGlowOpacity, m_laserGlowOpacity).toDouble()); + setLaserGlowColor(values.value(::settings::laserGlowColor, m_laserGlowColor).value()); + setLaserTrail(values.value(::settings::laserTrail, m_laserTrail).toBool()); + setLaserTrailTime(values.value(::settings::laserTrailTime, m_laserTrailTime).toInt()); + setLaserTrailWidth(values.value(::settings::laserTrailWidth, m_laserTrailWidth).toInt()); + setLaserTrailColor(values.value(::settings::laserTrailColor, m_laserTrailColor).value()); + setLaserTrailOpacity(values.value(::settings::laserTrailOpacity, m_laserTrailOpacity).toDouble()); setMultiScreenOverlayEnabled( values.value(::settings::multiScreenOverlay, m_multiScreenOverlayEnabled).toBool()); @@ -540,6 +655,19 @@ void Settings::setDefaults() setZoomEnabled(settings::defaultValue::zoomEnabled); setZoomFactor(settings::defaultValue::zoomFactor); setZoomMode(settings::defaultValue::zoomMode); + setPointerMode(QString(settings::defaultValue::pointerMode)); + setLaserSize(settings::defaultValue::laserSize); + setLaserColor(QColor(settings::defaultValue::laserColor)); + setLaserOpacity(settings::defaultValue::laserOpacity); + setLaserGlow(settings::defaultValue::laserGlow); + setLaserGlowSize(settings::defaultValue::laserGlowSize); + setLaserGlowOpacity(settings::defaultValue::laserGlowOpacity); + setLaserGlowColor(QColor(settings::defaultValue::laserGlowColor)); + setLaserTrail(settings::defaultValue::laserTrail); + setLaserTrailTime(settings::defaultValue::laserTrailTime); + setLaserTrailWidth(settings::defaultValue::laserTrailWidth); + setLaserTrailColor(QColor(settings::defaultValue::laserTrailColor)); + setLaserTrailOpacity(settings::defaultValue::laserTrailOpacity); setMultiScreenOverlayEnabled(settings::defaultValue::multiScreenOverlay); shapeSettingsSetDefaults(); } @@ -709,6 +837,19 @@ void Settings::load(const QString& preset) setZoomEnabled(m_config->zoomEnabled()); setZoomFactor(m_config->zoomFactor()); setZoomMode(m_config->zoomMode()); + setPointerMode(m_config->pointerMode()); + setLaserSize(m_config->laserSize()); + setLaserColor(m_config->laserColor()); + setLaserOpacity(m_config->laserOpacity()); + setLaserGlow(m_config->laserGlow()); + setLaserGlowSize(m_config->laserGlowSize()); + setLaserGlowOpacity(m_config->laserGlowOpacity()); + setLaserGlowColor(m_config->laserGlowColor()); + setLaserTrail(m_config->laserTrail()); + setLaserTrailTime(m_config->laserTrailTime()); + setLaserTrailWidth(m_config->laserTrailWidth()); + setLaserTrailColor(m_config->laserTrailColor()); + setLaserTrailOpacity(m_config->laserTrailOpacity()); setMultiScreenOverlayEnabled(m_config->multiScreenOverlay()); shapeSettingsLoad(); return; @@ -733,6 +874,19 @@ void Settings::load(const QString& preset) setZoomEnabled(readValue(s+::settings::zoomEnabled, settings::defaultValue::zoomEnabled).toBool()); setZoomFactor(readValue(s+::settings::zoomFactor, settings::defaultValue::zoomFactor).toDouble()); setZoomMode(readValue(s+::settings::zoomMode, settings::defaultValue::zoomMode).toString()); + setPointerMode(readValue(s+::settings::pointerMode, settings::defaultValue::pointerMode).toString()); + setLaserSize(readValue(s+::settings::laserSize, settings::defaultValue::laserSize).toInt()); + setLaserColor(readValue(s+::settings::laserColor, QColor(settings::defaultValue::laserColor)).value()); + setLaserOpacity(readValue(s+::settings::laserOpacity, settings::defaultValue::laserOpacity).toDouble()); + setLaserGlow(readValue(s+::settings::laserGlow, settings::defaultValue::laserGlow).toBool()); + setLaserGlowSize(readValue(s+::settings::laserGlowSize, settings::defaultValue::laserGlowSize).toInt()); + setLaserGlowOpacity(readValue(s+::settings::laserGlowOpacity, settings::defaultValue::laserGlowOpacity).toDouble()); + setLaserGlowColor(readValue(s+::settings::laserGlowColor, QColor(settings::defaultValue::laserGlowColor)).value()); + setLaserTrail(readValue(s+::settings::laserTrail, settings::defaultValue::laserTrail).toBool()); + setLaserTrailTime(readValue(s+::settings::laserTrailTime, settings::defaultValue::laserTrailTime).toInt()); + setLaserTrailWidth(readValue(s+::settings::laserTrailWidth, settings::defaultValue::laserTrailWidth).toInt()); + setLaserTrailColor(readValue(s+::settings::laserTrailColor, QColor(settings::defaultValue::laserTrailColor)).value()); + setLaserTrailOpacity(readValue(s+::settings::laserTrailOpacity, settings::defaultValue::laserTrailOpacity).toDouble()); setMultiScreenOverlayEnabled(readValue(s+::settings::multiScreenOverlay, settings::defaultValue::multiScreenOverlay).toBool()); shapeSettingsLoad(preset); } @@ -760,6 +914,19 @@ void Settings::savePreset(const QString& preset) writeValue(section+::settings::zoomEnabled, m_zoomEnabled); writeValue(section+::settings::zoomFactor, m_zoomFactor); writeValue(section+::settings::zoomMode, m_zoomMode); + writeValue(section+::settings::pointerMode, m_pointerMode); + writeValue(section+::settings::laserSize, m_laserSize); + writeValue(section+::settings::laserColor, m_laserColor); + writeValue(section+::settings::laserOpacity, m_laserOpacity); + writeValue(section+::settings::laserGlow, m_laserGlow); + writeValue(section+::settings::laserGlowSize, m_laserGlowSize); + writeValue(section+::settings::laserGlowOpacity, m_laserGlowOpacity); + writeValue(section+::settings::laserGlowColor, m_laserGlowColor); + writeValue(section+::settings::laserTrail, m_laserTrail); + writeValue(section+::settings::laserTrailTime, m_laserTrailTime); + writeValue(section+::settings::laserTrailWidth, m_laserTrailWidth); + writeValue(section+::settings::laserTrailColor, m_laserTrailColor); + writeValue(section+::settings::laserTrailOpacity, m_laserTrailOpacity); writeValue(section+::settings::multiScreenOverlay, m_multiScreenOverlayEnabled); shapeSettingsSavePreset(preset); @@ -1041,6 +1208,173 @@ void Settings::setZoomMode(const QString& mode) emit zoomModeChanged(m_zoomMode); } +// ------------------------------------------------------------------------------------------------- +bool Settings::isPointerMode(const QString& mode) +{ + const auto m = mode.trimmed().toLower(); + return m == QStringLiteral("spotlight") || m == QStringLiteral("laser"); +} + +// ------------------------------------------------------------------------------------------------- +void Settings::setPointerMode(const QString& mode) +{ + const auto normalizedMode = mode.trimmed().toLower(); + if (!isPointerMode(normalizedMode) || normalizedMode == m_pointerMode) { return; } + + m_pointerMode = normalizedMode; + m_config->setPointerMode(m_pointerMode); + save(); + qCDebug(PROJECTEUR_SETTINGS_LOG).noquote() << "pointer.mode =" << m_pointerMode; + emit pointerModeChanged(m_pointerMode); +} + +// ------------------------------------------------------------------------------------------------- +void Settings::setLaserSize(int size) +{ + if (size == m_laserSize) { return; } + + m_laserSize = qMin(qMax(::settings::ranges::laserSize.min, size), ::settings::ranges::laserSize.max); + m_config->setLaserSize(m_laserSize); + save(); + qCDebug(PROJECTEUR_SETTINGS_LOG).noquote() << "laser.size =" << m_laserSize; + emit laserSizeChanged(m_laserSize); +} + +// ------------------------------------------------------------------------------------------------- +void Settings::setLaserColor(const QColor& color) +{ + if (color == m_laserColor) { return; } + + m_laserColor = color; + m_config->setLaserColor(m_laserColor); + save(); + qCDebug(PROJECTEUR_SETTINGS_LOG).noquote() << "laser.color =" << m_laserColor.name(); + emit laserColorChanged(m_laserColor); +} + +// ------------------------------------------------------------------------------------------------- +void Settings::setLaserOpacity(double opacity) +{ + if (opacity > m_laserOpacity || opacity < m_laserOpacity) + { + m_laserOpacity = qMin(qMax(::settings::ranges::laserOpacity.min, opacity), ::settings::ranges::laserOpacity.max); + m_config->setLaserOpacity(m_laserOpacity); + save(); + qCDebug(PROJECTEUR_SETTINGS_LOG).noquote() << "laser.opacity =" << m_laserOpacity; + emit laserOpacityChanged(m_laserOpacity); + } +} + +// ------------------------------------------------------------------------------------------------- +void Settings::setLaserGlow(bool glow) +{ + if (glow == m_laserGlow) { return; } + + m_laserGlow = glow; + m_config->setLaserGlow(m_laserGlow); + save(); + qCDebug(PROJECTEUR_SETTINGS_LOG).noquote() << "laser.glow =" << m_laserGlow; + emit laserGlowChanged(m_laserGlow); +} + +// ------------------------------------------------------------------------------------------------- +void Settings::setLaserGlowSize(int size) +{ + if (size == m_laserGlowSize) { return; } + + m_laserGlowSize = qMin(qMax(::settings::ranges::laserGlowSize.min, size), ::settings::ranges::laserGlowSize.max); + m_config->setLaserGlowSize(m_laserGlowSize); + save(); + qCDebug(PROJECTEUR_SETTINGS_LOG).noquote() << "laser.glow-size =" << m_laserGlowSize; + emit laserGlowSizeChanged(m_laserGlowSize); +} + +// ------------------------------------------------------------------------------------------------- +void Settings::setLaserGlowOpacity(double opacity) +{ + if (opacity > m_laserGlowOpacity || opacity < m_laserGlowOpacity) + { + m_laserGlowOpacity = qMin(qMax(::settings::ranges::laserGlowOpacity.min, opacity), ::settings::ranges::laserGlowOpacity.max); + m_config->setLaserGlowOpacity(m_laserGlowOpacity); + save(); + qCDebug(PROJECTEUR_SETTINGS_LOG).noquote() << "laser.glow-opacity =" << m_laserGlowOpacity; + emit laserGlowOpacityChanged(m_laserGlowOpacity); + } +} + +// ------------------------------------------------------------------------------------------------- +void Settings::setLaserGlowColor(const QColor& color) +{ + if (color == m_laserGlowColor) { return; } + + m_laserGlowColor = color; + m_config->setLaserGlowColor(m_laserGlowColor); + save(); + qCDebug(PROJECTEUR_SETTINGS_LOG).noquote() << "laser.glow-color =" << m_laserGlowColor.name(); + emit laserGlowColorChanged(m_laserGlowColor); +} + +// ------------------------------------------------------------------------------------------------- +void Settings::setLaserTrail(bool trail) +{ + if (trail == m_laserTrail) { return; } + + m_laserTrail = trail; + m_config->setLaserTrail(m_laserTrail); + save(); + qCDebug(PROJECTEUR_SETTINGS_LOG).noquote() << "laser.trail =" << m_laserTrail; + emit laserTrailChanged(m_laserTrail); +} + +// ------------------------------------------------------------------------------------------------- +void Settings::setLaserTrailTime(int timeMs) +{ + if (timeMs == m_laserTrailTime) { return; } + + m_laserTrailTime = qMin(qMax(::settings::ranges::laserTrailTime.min, timeMs), ::settings::ranges::laserTrailTime.max); + m_config->setLaserTrailTime(m_laserTrailTime); + save(); + qCDebug(PROJECTEUR_SETTINGS_LOG).noquote() << "laser.trail.time =" << m_laserTrailTime; + emit laserTrailTimeChanged(m_laserTrailTime); +} + +// ------------------------------------------------------------------------------------------------- +void Settings::setLaserTrailWidth(int width) +{ + if (width == m_laserTrailWidth) { return; } + + m_laserTrailWidth = qMin(qMax(::settings::ranges::laserTrailWidth.min, width), ::settings::ranges::laserTrailWidth.max); + m_config->setLaserTrailWidth(m_laserTrailWidth); + save(); + qCDebug(PROJECTEUR_SETTINGS_LOG).noquote() << "laser.trail.width =" << m_laserTrailWidth; + emit laserTrailWidthChanged(m_laserTrailWidth); +} + +// ------------------------------------------------------------------------------------------------- +void Settings::setLaserTrailColor(const QColor& color) +{ + if (color == m_laserTrailColor) { return; } + + m_laserTrailColor = color; + m_config->setLaserTrailColor(m_laserTrailColor); + save(); + qCDebug(PROJECTEUR_SETTINGS_LOG).noquote() << "laser.trail.color =" << m_laserTrailColor.name(); + emit laserTrailColorChanged(m_laserTrailColor); +} + +// ------------------------------------------------------------------------------------------------- +void Settings::setLaserTrailOpacity(double opacity) +{ + if (opacity > m_laserTrailOpacity || opacity < m_laserTrailOpacity) + { + m_laserTrailOpacity = qMin(qMax(::settings::ranges::laserTrailOpacity.min, opacity), ::settings::ranges::laserTrailOpacity.max); + m_config->setLaserTrailOpacity(m_laserTrailOpacity); + save(); + qCDebug(PROJECTEUR_SETTINGS_LOG).noquote() << "laser.trail.opacity =" << m_laserTrailOpacity; + emit laserTrailOpacityChanged(m_laserTrailOpacity); + } +} + // ------------------------------------------------------------------------------------------------- void Settings::setMultiScreenOverlayEnabled(bool enabled) { diff --git a/src/settings.h b/src/settings.h index 8f41796c..7cbd5d20 100644 --- a/src/settings.h +++ b/src/settings.h @@ -42,6 +42,19 @@ class Settings : public QObject Q_PROPERTY(bool zoomEnabled READ zoomEnabled WRITE setZoomEnabled NOTIFY zoomEnabledChanged) Q_PROPERTY(double zoomFactor READ zoomFactor WRITE setZoomFactor NOTIFY zoomFactorChanged) Q_PROPERTY(QString zoomMode READ zoomMode WRITE setZoomMode NOTIFY zoomModeChanged) + Q_PROPERTY(QString pointerMode READ pointerMode WRITE setPointerMode NOTIFY pointerModeChanged) + Q_PROPERTY(int laserSize READ laserSize WRITE setLaserSize NOTIFY laserSizeChanged) + Q_PROPERTY(QColor laserColor READ laserColor WRITE setLaserColor NOTIFY laserColorChanged) + Q_PROPERTY(double laserOpacity READ laserOpacity WRITE setLaserOpacity NOTIFY laserOpacityChanged) + Q_PROPERTY(bool laserGlow READ laserGlow WRITE setLaserGlow NOTIFY laserGlowChanged) + Q_PROPERTY(int laserGlowSize READ laserGlowSize WRITE setLaserGlowSize NOTIFY laserGlowSizeChanged) + Q_PROPERTY(double laserGlowOpacity READ laserGlowOpacity WRITE setLaserGlowOpacity NOTIFY laserGlowOpacityChanged) + Q_PROPERTY(QColor laserGlowColor READ laserGlowColor WRITE setLaserGlowColor NOTIFY laserGlowColorChanged) + Q_PROPERTY(bool laserTrail READ laserTrail WRITE setLaserTrail NOTIFY laserTrailChanged) + Q_PROPERTY(int laserTrailTime READ laserTrailTime WRITE setLaserTrailTime NOTIFY laserTrailTimeChanged) + Q_PROPERTY(int laserTrailWidth READ laserTrailWidth WRITE setLaserTrailWidth NOTIFY laserTrailWidthChanged) + Q_PROPERTY(QColor laserTrailColor READ laserTrailColor WRITE setLaserTrailColor NOTIFY laserTrailColorChanged) + Q_PROPERTY(double laserTrailOpacity READ laserTrailOpacity WRITE setLaserTrailOpacity NOTIFY laserTrailOpacityChanged) Q_PROPERTY(bool multiScreenOverlayEnabled READ multiScreenOverlayEnabled WRITE setMultiScreenOverlayEnabled NOTIFY multiScreenOverlayEnabledChanged) public: @@ -88,6 +101,33 @@ class Settings : public QObject void setZoomFactor(double factor); QString zoomMode() const { return m_zoomMode; } void setZoomMode(const QString& mode); + QString pointerMode() const { return m_pointerMode; } + void setPointerMode(const QString& mode); + static bool isPointerMode(const QString& mode); + int laserSize() const { return m_laserSize; } + void setLaserSize(int size); + QColor laserColor() const { return m_laserColor; } + void setLaserColor(const QColor& color); + double laserOpacity() const { return m_laserOpacity; } + void setLaserOpacity(double opacity); + bool laserGlow() const { return m_laserGlow; } + void setLaserGlow(bool glow); + int laserGlowSize() const { return m_laserGlowSize; } + void setLaserGlowSize(int size); + double laserGlowOpacity() const { return m_laserGlowOpacity; } + void setLaserGlowOpacity(double opacity); + QColor laserGlowColor() const { return m_laserGlowColor; } + void setLaserGlowColor(const QColor& color); + bool laserTrail() const { return m_laserTrail; } + void setLaserTrail(bool trail); + int laserTrailTime() const { return m_laserTrailTime; } + void setLaserTrailTime(int timeMs); + int laserTrailWidth() const { return m_laserTrailWidth; } + void setLaserTrailWidth(int width); + QColor laserTrailColor() const { return m_laserTrailColor; } + void setLaserTrailColor(const QColor& color); + double laserTrailOpacity() const { return m_laserTrailOpacity; } + void setLaserTrailOpacity(double opacity); bool multiScreenOverlayEnabled() const { return m_multiScreenOverlayEnabled; } void setMultiScreenOverlayEnabled(bool enabled); bool overlayDisabled() const { return m_overlayDisabled; } @@ -106,6 +146,13 @@ class Settings : public QObject static const SettingRange& borderSizeRange(); static const SettingRange& borderOpacityRange(); static const SettingRange& zoomFactorRange(); + static const SettingRange& laserSizeRange(); + static const SettingRange& laserOpacityRange(); + static const SettingRange& laserGlowSizeRange(); + static const SettingRange& laserGlowOpacityRange(); + static const SettingRange& laserTrailTimeRange(); + static const SettingRange& laserTrailWidthRange(); + static const SettingRange& laserTrailOpacityRange(); static const SettingRange& inputSequenceIntervalRange(); class SpotShapeSetting { @@ -209,6 +256,19 @@ class Settings : public QObject void zoomEnabledChanged(bool enabled); void zoomFactorChanged(double zoomFactor); void zoomModeChanged(const QString& mode); + void pointerModeChanged(const QString& mode); + void laserSizeChanged(int size); + void laserColorChanged(const QColor& color); + void laserOpacityChanged(double opacity); + void laserGlowChanged(bool glow); + void laserGlowSizeChanged(int size); + void laserGlowOpacityChanged(double opacity); + void laserGlowColorChanged(const QColor& color); + void laserTrailChanged(bool trail); + void laserTrailTimeChanged(int timeMs); + void laserTrailWidthChanged(int width); + void laserTrailColorChanged(const QColor& color); + void laserTrailOpacityChanged(double opacity); void multiScreenOverlayEnabledChanged(bool enabled); void overlayDisabledChanged(bool disabled); @@ -236,6 +296,19 @@ class Settings : public QObject bool m_zoomEnabled = false; double m_zoomFactor = 2.0; QString m_zoomMode = QStringLiteral("smooth"); + QString m_pointerMode = QStringLiteral("spotlight"); + int m_laserSize = 16; + QColor m_laserColor; + double m_laserOpacity = 0.9; + bool m_laserGlow = true; + int m_laserGlowSize = 10; + double m_laserGlowOpacity = 0.6; + QColor m_laserGlowColor; + bool m_laserTrail = false; + int m_laserTrailTime = 400; + int m_laserTrailWidth = 4; + QColor m_laserTrailColor; + double m_laserTrailOpacity = 0.8; bool m_showSpotShade = true; bool m_showCenterDot = false; bool m_spotRotationAllowed = false; diff --git a/src/spotlight.cc b/src/spotlight.cc index 7172903b..ff006a88 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -298,6 +298,23 @@ int Spotlight::connectDevices() { m_settings->setOverlayDisabled(!m_settings->overlayDisabled()); } + else if (action->type() == Action::Type::ToggleLaser) + { + if (m_settings->pointerMode() != QStringLiteral("laser")) { + m_settings->setPointerMode(QStringLiteral("laser")); + if (m_settings->overlayDisabled()) { + m_settings->setOverlayDisabled(false); + } + } else { + m_settings->setOverlayDisabled(!m_settings->overlayDisabled()); + } + } + else if (action->type() == Action::Type::TogglePointerMode) + { + m_settings->setPointerMode(m_settings->pointerMode() == QStringLiteral("laser") + ? QStringLiteral("spotlight") + : QStringLiteral("laser")); + } else if (action->type() == Action::Type::ScrollHorizontal || action->type() == Action::Type::ScrollVertical) { if (!m_virtualMouseDevice) { return; }