diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 7f065eed68..119c8edc9e 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -28,7 +28,6 @@ jobs:
linuxmint/cinnamon-control-center,
linuxmint/cinnamon-desktop,
linuxmint/cinnamon-menus,
- linuxmint/cinnamon-screensaver,
linuxmint/cinnamon-session,
linuxmint/cinnamon-settings-daemon,
linuxmint/cinnamon-translations,
diff --git a/.github/workflows/pattern-checker.yml b/.github/workflows/pattern-checker.yml
new file mode 100644
index 0000000000..a92e21d4ca
--- /dev/null
+++ b/.github/workflows/pattern-checker.yml
@@ -0,0 +1,25 @@
+name: Pattern Check
+
+on:
+ pull_request_target:
+ branches: [ master ]
+
+permissions:
+ pull-requests: write
+
+jobs:
+ pattern-check:
+ name: Pattern Check
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout github-actions
+ uses: actions/checkout@v6
+ with:
+ repository: linuxmint/github-actions
+ path: _github-actions
+
+ - name: Pattern Check
+ uses: ./_github-actions/pattern-checker
+ with:
+ github_token: ${{ github.token }}
diff --git a/cinnamon.session.in b/cinnamon.session.in
index 9191df0d79..7999775f36 100644
--- a/cinnamon.session.in
+++ b/cinnamon.session.in
@@ -1,6 +1,6 @@
[Cinnamon Session]
Name=Cinnamon
-RequiredComponents=cinnamon;org.cinnamon.ScreenSaver;nemo-autostart;@REQUIRED@cinnamon-killer-daemon;
+RequiredComponents=cinnamon;nemo-autostart;@REQUIRED@cinnamon-killer-daemon;
DesktopName=X-Cinnamon
diff --git a/cinnamon2d.session.in b/cinnamon2d.session.in
index 8910f998c6..e15e849870 100644
--- a/cinnamon2d.session.in
+++ b/cinnamon2d.session.in
@@ -1,6 +1,6 @@
[Cinnamon Session]
Name=Cinnamon (Software Rendering)
-RequiredComponents=cinnamon2d;org.cinnamon.ScreenSaver;nemo-autostart;@REQUIRED@cinnamon-killer-daemon;
+RequiredComponents=cinnamon2d;nemo-autostart;@REQUIRED@cinnamon-killer-daemon;
DesktopName=X-Cinnamon
diff --git a/data/meson.build b/data/meson.build
index 58e2e03822..1738bc4a0e 100644
--- a/data/meson.build
+++ b/data/meson.build
@@ -62,3 +62,5 @@ gnome.compile_resources(
install: true,
install_dir: pkgdatadir
)
+
+subdir('pam')
diff --git a/data/org.cinnamon.gschema.xml b/data/org.cinnamon.gschema.xml
index 46d0315d59..e01fd3a313 100644
--- a/data/org.cinnamon.gschema.xml
+++ b/data/org.cinnamon.gschema.xml
@@ -472,17 +472,6 @@
-
- false
- Whether edge flip is enabled
-
-
-
- 1000
- Duration of the delay before switching the workspace
- Duration of the delay (in milliseconds)
-
-
false
Whether advanced mode is enabled in cinnamon-settings
@@ -532,6 +521,24 @@
If true, the pointer will be set to the center of the new monitor when using pointer next/previous shortcuts.
+
+ true
+ Use internal screensaver implementation. Requires cinnamon restart if changed.
+ If true, use Cinnamon's internal screensaver for locking instead of the external cinnamon-screensaver daemon.
+
+
+
+ false
+ Whether the session is currently locked
+ Persists the screensaver locked state so it can be restored after a Cinnamon restart. This key is managed internally and should not be modified manually.
+
+
+
+ false
+ Enable screensaver debug logging
+ If true, enables verbose debug logging for the screensaver, unlock dialog, and backup-locker.
+
+
diff --git a/data/pam/cinnamon.pam b/data/pam/cinnamon.pam
new file mode 100644
index 0000000000..dde6081926
--- /dev/null
+++ b/data/pam/cinnamon.pam
@@ -0,0 +1,16 @@
+#%PAM-1.0
+
+# Fedora & Arch
+-auth sufficient pam_selinux_permit.so
+auth include system-auth
+-auth optional pam_gnome_keyring.so
+account include system-auth
+password include system-auth
+session include system-auth
+
+# SuSE/Novell
+#auth include common-auth
+#auth optional pam_gnome_keyring.so
+#account include common-account
+#password include common-password
+#session include common-session
diff --git a/data/pam/cinnamon.pam.debian b/data/pam/cinnamon.pam.debian
new file mode 100644
index 0000000000..a9fd9cefed
--- /dev/null
+++ b/data/pam/cinnamon.pam.debian
@@ -0,0 +1,2 @@
+@include common-auth
+auth optional pam_gnome_keyring.so
diff --git a/data/pam/meson.build b/data/pam/meson.build
new file mode 100644
index 0000000000..1df604b992
--- /dev/null
+++ b/data/pam/meson.build
@@ -0,0 +1,18 @@
+pamdir = get_option('pam_prefix')
+if pamdir == ''
+ pamdir = sysconfdir
+endif
+
+if get_option('use_debian_pam')
+ install_data(
+ 'cinnamon.pam.debian',
+ rename: 'cinnamon',
+ install_dir: join_paths(pamdir, 'pam.d')
+ )
+else
+ install_data(
+ 'cinnamon.pam',
+ rename: 'cinnamon',
+ install_dir: join_paths(pamdir, 'pam.d')
+ )
+endif
diff --git a/data/services/meson.build b/data/services/meson.build
index 2fe651c2e9..9b9e3833f2 100644
--- a/data/services/meson.build
+++ b/data/services/meson.build
@@ -1,4 +1,5 @@
service_files = [
+ 'org.cinnamon.BackupLocker.service',
'org.cinnamon.CalendarServer.service',
'org.Cinnamon.HotplugSniffer.service',
'org.Cinnamon.Melange.service',
diff --git a/data/services/org.cinnamon.BackupLocker.service.in b/data/services/org.cinnamon.BackupLocker.service.in
new file mode 100644
index 0000000000..5ee4a15b48
--- /dev/null
+++ b/data/services/org.cinnamon.BackupLocker.service.in
@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=org.cinnamon.BackupLocker
+Exec=@libexecdir@/cinnamon-backup-locker
diff --git a/data/theme/add-workspace-hover.svg b/data/theme/add-workspace-hover.svg
index adbf1f5aee..863a7ede34 100644
--- a/data/theme/add-workspace-hover.svg
+++ b/data/theme/add-workspace-hover.svg
@@ -25,13 +25,13 @@
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="16"
- inkscape:cx="23.8125"
- inkscape:cy="114.25"
+ inkscape:cx="21.5"
+ inkscape:cy="114.34375"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
- inkscape:window-height="1008"
+ inkscape:window-height="1000"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
@@ -84,7 +84,7 @@
style="fill:#000000;fill-opacity:1;opacity:0.5" />
= 5.3),
libnm-dev (>= 1.6) [linux-any],
libnma-dev [linux-any],
+ libpam0g-dev,
libpolkit-agent-1-dev (>= 0.100),
libpulse-dev,
librsvg2-dev,
libsecret-1-dev,
libstartup-notification0-dev (>= 0.11),
libxapp-dev (>= 2.6.0),
+ libxcomposite-dev (>= 1:0.4),
+ libxdo-dev,
meson,
pysassc,
python3:any,
@@ -45,7 +48,6 @@ Depends:
cinnamon-control-center,
cinnamon-desktop-data (>= 5.3),
cinnamon-l10n,
- cinnamon-screensaver,
cinnamon-session,
cinnamon-settings-daemon (>= 5.3),
cjs (>= 4.8),
@@ -118,7 +120,10 @@ Recommends:
gnome-online-accounts-gtk,
touchegg,
ibus,
+ libpam-gnome-keyring,
Suggests: cinnamon-doc
+Breaks: cinnamon-screensaver (<< 6.7)
+Replaces: cinnamon-screensaver (<< 6.7)
Provides: notification-daemon, x-window-manager, polkit-1-auth-agent
Description: Modern Linux desktop
Cinnamon is a modern Linux desktop which provides advanced innovative
diff --git a/debian/rules b/debian/rules
index ae788e3a3c..af66079feb 100755
--- a/debian/rules
+++ b/debian/rules
@@ -20,7 +20,8 @@ override_dh_auto_configure:
-D deprecated_warnings=false \
-D exclude_info_settings=true \
-D exclude_users_settings=true \
- -D py3modules_dir=/usr/lib/python3/dist-packages
+ -D py3modules_dir=/usr/lib/python3/dist-packages \
+ -D use_debian_pam=true
# workaround for fix lmde4 build
override_dh_dwz:
diff --git a/docs/reference/cinnamon/meson.build b/docs/reference/cinnamon/meson.build
index 4514ff5d2c..0e63e730fd 100644
--- a/docs/reference/cinnamon/meson.build
+++ b/docs/reference/cinnamon/meson.build
@@ -5,6 +5,7 @@ ignore = [
st_private_headers,
tray_headers,
sniffer_headers,
+ backup_locker_headers,
]
if not internal_nm_agent
diff --git a/files/usr/bin/cinnamon-install-spice b/files/usr/bin/cinnamon-install-spice
index 370239d68b..e58b8b1595 100755
--- a/files/usr/bin/cinnamon-install-spice
+++ b/files/usr/bin/cinnamon-install-spice
@@ -7,8 +7,8 @@ import argparse
import gi
gi.require_version('Gtk', '3.0')
-sys.path.append('/usr/share/cinnamon/cinnamon-settings/bin')
-from Spices import Spice_Harvester
+sys.path.append('/usr/share/cinnamon/cinnamon-settings')
+from bin.Spices import Spice_Harvester
USAGE_DESCRIPTION = 'Installs an applet, desklet, extension, or theme from a local folder. Rather than just doing a shallow copy, it will also install translations, schema files (if present) and update the metadata with a timestamp for version comparison.'
USAGE_EPILOG = 'This script is designed for developers to test their work. It is recommended that everyone else continue to use cinnamon-settings to install their spices as this script has no safeguards in place to prevent malicious code.'
diff --git a/files/usr/bin/cinnamon-launcher b/files/usr/bin/cinnamon-launcher
index c19e83b7ee..06404286b8 100755
--- a/files/usr/bin/cinnamon-launcher
+++ b/files/usr/bin/cinnamon-launcher
@@ -20,6 +20,22 @@ from gi.repository import Gtk, GLib, Gio, GLib
FALLBACK_COMMAND = "metacity"
FALLBACK_ARGS = ("--replace",)
+LAUNCHER_BUS_NAME = "org.cinnamon.Launcher"
+LAUNCHER_BUS_PATH = "/org/cinnamon/Launcher"
+
+INTERFACE_XML = (
+ ""
+ " "
+ " "
+ " "
+ " "
+ " "
+ " "
+ " "
+ " "
+ ""
+)
+
gettext.install("cinnamon", "/usr/share/locale")
panel_process_name = None
@@ -57,6 +73,8 @@ class Launcher:
self.polkit_agent_proc = None
self.nm_applet_proc = None
+ self.can_restart = False
+ self.dialog = None
self.cinnamon_pid = os.fork()
if self.cinnamon_pid == 0:
@@ -68,6 +86,7 @@ class Launcher:
if self.settings.get_boolean("memory-limit-enabled"):
print("Cinnamon memory limit enabled: %d MB" % self.settings.get_int("memory-limit"))
self.monitor_memory()
+ self.setup_dbus()
self.wait_for_process()
Gtk.main()
@@ -75,6 +94,35 @@ class Launcher:
print("Memory profiler status changed, restarting Cinnamon.")
self.restart_cinnamon()
+ def setup_dbus(self):
+ self.node_info = Gio.DBusNodeInfo.new_for_xml(INTERFACE_XML)
+ Gio.bus_own_name(
+ Gio.BusType.SESSION,
+ LAUNCHER_BUS_NAME,
+ Gio.BusNameOwnerFlags.NONE,
+ self.on_bus_acquired,
+ None,
+ None
+ )
+
+ def on_bus_acquired(self, connection, name):
+ connection.register_object(
+ LAUNCHER_BUS_PATH,
+ self.node_info.interfaces[0],
+ self.on_method_call
+ )
+
+ def on_method_call(self, connection, sender, object_path,
+ interface_name, method_name, parameters, invocation):
+ if method_name == "CanRestart":
+ invocation.return_value(GLib.Variant("(b)", (self.can_restart,)))
+ elif method_name == "TryRestart":
+ result = False
+ if self.can_restart and self.dialog is not None:
+ GLib.idle_add(self.dialog.response, Gtk.ResponseType.YES)
+ result = True
+ invocation.return_value(GLib.Variant("(b)", (result,)))
+
@async_function
def wait_for_process(self):
exit_status = os.waitpid(self.cinnamon_pid, 0)[1]
@@ -168,6 +216,8 @@ class Launcher:
@idle_function
def confirm_restart(self):
+ self.can_restart = True
+
d = Gtk.MessageDialog(title=None, parent=None, flags=0, message_type=Gtk.MessageType.WARNING)
d.add_buttons(_("No"), Gtk.ResponseType.NO,
_("Yes"), Gtk.ResponseType.YES)
@@ -185,7 +235,11 @@ class Launcher:
box.set_border_width(20)
box.add(checkbutton)
checkbutton.show_all()
+
+ self.dialog = d
resp = d.run()
+ self.dialog = None
+ self.can_restart = False
d.destroy()
if resp == Gtk.ResponseType.YES:
if checkbutton.get_active():
diff --git a/files/usr/bin/cinnamon-screensaver-command b/files/usr/bin/cinnamon-screensaver-command
new file mode 100755
index 0000000000..709d06cf90
--- /dev/null
+++ b/files/usr/bin/cinnamon-screensaver-command
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+exec /usr/share/cinnamon/cinnamon-screensaver-command/cinnamon-screensaver-command.py "$@"
diff --git a/files/usr/bin/cinnamon-screensaver-lock-dialog b/files/usr/bin/cinnamon-screensaver-lock-dialog
deleted file mode 100755
index b6e51ae9a6..0000000000
--- a/files/usr/bin/cinnamon-screensaver-lock-dialog
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/python3
-
-""" Launch the cinnamon screensaver lock dialog
-"""
-
-import os
-
-os.system("/usr/share/cinnamon/cinnamon-screensaver-lock-dialog/cinnamon-screensaver-lock-dialog.py")
diff --git a/files/usr/share/cinnamon/applets/calendar@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/calendar@cinnamon.org/applet.js
index 7b9af66822..436bce96b3 100644
--- a/files/usr/share/cinnamon/applets/calendar@cinnamon.org/applet.js
+++ b/files/usr/share/cinnamon/applets/calendar@cinnamon.org/applet.js
@@ -8,12 +8,14 @@ const Util = imports.misc.util;
const PopupMenu = imports.ui.popupMenu;
const UPowerGlib = imports.gi.UPowerGlib;
const Settings = imports.ui.settings;
-const Calendar = require('./calendar');
-const EventView = require('./eventView');
const CinnamonDesktop = imports.gi.CinnamonDesktop;
const Main = imports.ui.main;
const Separator = imports.ui.separator;
+const Me = imports.ui.extension.getCurrentExtension();
+const Calendar = Me.imports.calendar;
+const EventView = Me.imports.eventView;
+
const DAY_FORMAT = CinnamonDesktop.WallClock.lctime_format("cinnamon", "%A");
const DATE_FORMAT_SHORT = CinnamonDesktop.WallClock.lctime_format("cinnamon", _("%B %-e, %Y"));
const DATE_FORMAT_FULL = CinnamonDesktop.WallClock.lctime_format("cinnamon", _("%A, %B %-e, %Y"));
diff --git a/files/usr/share/cinnamon/applets/calendar@cinnamon.org/calendar.js b/files/usr/share/cinnamon/applets/calendar@cinnamon.org/calendar.js
index 05c9ddfed0..24bc303bf9 100644
--- a/files/usr/share/cinnamon/applets/calendar@cinnamon.org/calendar.js
+++ b/files/usr/share/cinnamon/applets/calendar@cinnamon.org/calendar.js
@@ -145,7 +145,7 @@ function _dateIntervalsOverlap(a0, a1, b0, b1)
return true;
}
-class Calendar {
+var Calendar = class Calendar {
constructor(settings, events_manager) {
this.events_manager = events_manager;
this._weekStart = Cinnamon.util_get_week_start();
diff --git a/files/usr/share/cinnamon/applets/calendar@cinnamon.org/eventView.js b/files/usr/share/cinnamon/applets/calendar@cinnamon.org/eventView.js
index aa3930f226..ad23d4d08a 100644
--- a/files/usr/share/cinnamon/applets/calendar@cinnamon.org/eventView.js
+++ b/files/usr/share/cinnamon/applets/calendar@cinnamon.org/eventView.js
@@ -290,7 +290,7 @@ class EventDataList {
}
}
-class EventsManager {
+var EventsManager = class EventsManager {
constructor(settings, desktop_settings) {
this.settings = settings;
this.desktop_settings = desktop_settings;
@@ -796,7 +796,7 @@ class EventList {
for (let event_data of events) {
if (first_row_done) {
- this.events_box.add_actor(new Separator.Separator().actor);
+ this.events_box.add_actor(new Separator.Separator());
}
let row = new EventRow(
diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js
index 8ca0467b56..1a12a801bc 100644
--- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js
+++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js
@@ -12,8 +12,9 @@ const Mainloop = imports.mainloop;
const {SignalManager} = imports.misc.signalManager;
const {unref} = imports.misc.util;
-const createStore = require('./state');
-const {AppMenuButtonRightClickMenu, HoverMenuController, AppThumbnailHoverMenu} = require('./menus');
+const Me = imports.ui.extension.getCurrentExtension();
+const {createStore} = Me.imports.state;
+const {AppMenuButtonRightClickMenu, HoverMenuController, AppThumbnailHoverMenu} = Me.imports.menus;
const {
FLASH_INTERVAL,
FLASH_MAX_COUNT,
@@ -21,7 +22,7 @@ const {
BUTTON_BOX_ANIMATION_TIME,
RESERVE_KEYS,
TitleDisplay
-} = require('./constants');
+} = Me.imports.constants;
const _reLetterRtl = new RegExp("\\p{Script=Hebrew}|\\p{Script=Arabic}", "u");
const _reLetter = new RegExp("\\p{L}", "u");
@@ -60,7 +61,7 @@ const getFocusState = function(metaWindow) {
return false;
};
-class AppGroup {
+var AppGroup = class AppGroup {
constructor(params) {
this.state = params.state;
this.workspaceState = params.workspaceState;
@@ -1224,5 +1225,3 @@ class AppGroup {
}
}
}
-
-module.exports = AppGroup;
diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js
index 24c944e17c..ba61cc1af5 100644
--- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js
+++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js
@@ -14,14 +14,15 @@ const {AppletSettings} = imports.ui.settings;
const {SignalManager} = imports.misc.signalManager;
const {throttle, unref, trySpawnCommandLine} = imports.misc.util;
-const createStore = require('./state');
-const AppGroup = require('./appGroup');
-const Workspace = require('./workspace');
+const Me = imports.ui.extension.getCurrentExtension();
+const {createStore} = Me.imports.state;
+const {AppGroup} = Me.imports.appGroup;
+const {Workspace} = Me.imports.workspace;
const {
- RESERVE_KEYS,
- TitleDisplay,
- autoStartStrDir
-} = require('./constants');
+ RESERVE_KEYS,
+ TitleDisplay,
+ autoStartStrDir
+} = Me.imports.constants;
class PinnedFavs {
constructor(params) {
diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/constants.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/constants.js
index 51b08467e4..4395c17bb0 100644
--- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/constants.js
+++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/constants.js
@@ -1,29 +1,24 @@
-const CLOSE_BTN_SIZE = 22;
-const constants = {
- CLOSE_BTN_SIZE,
- CLOSED_BUTTON_STYLE: 'padding: 0px; width: ' + CLOSE_BTN_SIZE + 'px; height: '
- + CLOSE_BTN_SIZE + 'px; max-width: ' + CLOSE_BTN_SIZE
- + 'px; max-height: ' + CLOSE_BTN_SIZE + 'px; ' + '-cinnamon-close-overlap: 0px;' +
- 'background-size: ' + CLOSE_BTN_SIZE + 'px ' + CLOSE_BTN_SIZE + 'px;',
- THUMBNAIL_ICON_SIZE: 16,
- OPACITY_OPAQUE: 255,
- BUTTON_BOX_ANIMATION_TIME: 150,
- MAX_BUTTON_WIDTH: 150, // Pixels
- FLASH_INTERVAL: 500,
- FLASH_MAX_COUNT: 4,
- RESERVE_KEYS: ['willUnmount'],
- TitleDisplay: {
- None: 1,
- App: 2,
- Title: 3,
- Focused: 4
- },
- FavType: {
- favorites: 0,
- pinnedApps: 1,
- none: 2
- },
- autoStartStrDir: './.config/autostart',
+var CLOSE_BTN_SIZE = 22;
+var CLOSED_BUTTON_STYLE = 'padding: 0px; width: ' + CLOSE_BTN_SIZE + 'px; height: '
+ + CLOSE_BTN_SIZE + 'px; max-width: ' + CLOSE_BTN_SIZE
+ + 'px; max-height: ' + CLOSE_BTN_SIZE + 'px; ' + '-cinnamon-close-overlap: 0px;' +
+ 'background-size: ' + CLOSE_BTN_SIZE + 'px ' + CLOSE_BTN_SIZE + 'px;';
+var THUMBNAIL_ICON_SIZE = 16;
+var OPACITY_OPAQUE = 255;
+var BUTTON_BOX_ANIMATION_TIME = 150;
+var MAX_BUTTON_WIDTH = 150; // Pixels
+var FLASH_INTERVAL = 500;
+var FLASH_MAX_COUNT = 4;
+var RESERVE_KEYS = ['willUnmount'];
+var TitleDisplay = {
+ None: 1,
+ App: 2,
+ Title: 3,
+ Focused: 4
};
-
-module.exports = constants;
+var FavType = {
+ favorites: 0,
+ pinnedApps: 1,
+ none: 2
+};
+var autoStartStrDir = './.config/autostart';
diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/menus.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/menus.js
index e176f20163..607b9cbd32 100644
--- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/menus.js
+++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/menus.js
@@ -10,6 +10,7 @@ const WindowUtils = imports.misc.windowUtils;
const Mainloop = imports.mainloop;
const {tryFn, unref, trySpawnCommandLine, spawn_async, getDesktopActionIcon} = imports.misc.util;
+const Me = imports.ui.extension.getCurrentExtension();
const {
CLOSE_BTN_SIZE,
CLOSED_BUTTON_STYLE,
@@ -17,7 +18,7 @@ const {
RESERVE_KEYS,
FavType,
autoStartStrDir
-} = require('./constants');
+} = Me.imports.constants;
const convertRange = function(value, r1, r2) {
return ((value - r1[0]) * (r2[1] - r2[0])) / (r1[1] - r1[0]) + r2[0];
@@ -39,7 +40,7 @@ const setOpacity = (peekTime, window_actor, targetOpacity, cb) => {
window_actor.ease(easeConfig);
};
-class AppMenuButtonRightClickMenu extends Applet.AppletPopupMenu {
+var AppMenuButtonRightClickMenu = class AppMenuButtonRightClickMenu extends Applet.AppletPopupMenu {
constructor(params, orientation) {
super(params, orientation);
this.state = params.state;
@@ -413,7 +414,7 @@ class AppMenuButtonRightClickMenu extends Applet.AppletPopupMenu {
}
}
-class HoverMenuController extends PopupMenu.PopupMenuManager {
+var HoverMenuController = class HoverMenuController extends PopupMenu.PopupMenuManager {
constructor(actor, groupState) {
super({actor}, false); // owner, shouldGrab
this.groupState = groupState;
@@ -860,7 +861,7 @@ class WindowThumbnail {
}
}
-class AppThumbnailHoverMenu extends PopupMenu.PopupMenu {
+var AppThumbnailHoverMenu = class AppThumbnailHoverMenu extends PopupMenu.PopupMenu {
_init(state, groupState) {
super._init.call(this, groupState.trigger('getActor'), state.orientation, 0.5);
this.state = state;
@@ -1227,9 +1228,3 @@ class AppThumbnailHoverMenu extends PopupMenu.PopupMenu {
unref(this, RESERVE_KEYS);
}
}
-
-module.exports = {
- AppMenuButtonRightClickMenu,
- HoverMenuController,
- AppThumbnailHoverMenu
-};
diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/state.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/state.js
index 714422334f..758e1d9a9e 100644
--- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/state.js
+++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/state.js
@@ -137,7 +137,7 @@ function clone(object, refs = [], cache = null) {
* to be used at the end of the application life cycle.
*
*/
-function createStore(state = {}, listeners = [], connections = 0) {
+var createStore = function(state = {}, listeners = [], connections = 0) {
const publicAPI = Object.freeze({
get,
set,
@@ -316,6 +316,4 @@ function createStore(state = {}, listeners = [], connections = 0) {
}
return getAPIWithObject(state);
-}
-
-module.exports = createStore;
\ No newline at end of file
+};
\ No newline at end of file
diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js
index 4af591ee8c..ff25863aa3 100644
--- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js
+++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js
@@ -3,11 +3,12 @@ const Main = imports.ui.main;
const {SignalManager} = imports.misc.signalManager;
const {unref} = imports.misc.util;
-const createStore = require('./state');
-const AppGroup = require('./appGroup');
-const {RESERVE_KEYS} = require('./constants');
+const Me = imports.ui.extension.getCurrentExtension();
+const {createStore} = Me.imports.state;
+const {AppGroup} = Me.imports.appGroup;
+const {RESERVE_KEYS} = Me.imports.constants;
-class Workspace {
+var Workspace = class Workspace {
constructor(params) {
this.state = params.state;
this.state.connect({
@@ -428,5 +429,3 @@ class Workspace {
unref(this, RESERVE_KEYS);
}
}
-
-module.exports = Workspace;
diff --git a/files/usr/share/cinnamon/applets/keyboard@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/keyboard@cinnamon.org/applet.js
index e5762512b5..6446feb338 100644
--- a/files/usr/share/cinnamon/applets/keyboard@cinnamon.org/applet.js
+++ b/files/usr/share/cinnamon/applets/keyboard@cinnamon.org/applet.js
@@ -161,7 +161,7 @@ class CinnamonKeyboardApplet extends Applet.Applet {
let actor = null;
if (this._inputSourcesManager.showFlags) {
- actor = this._inputSourcesManager.createFlagIcon(source, POPUP_MENU_ICON_STYLE_CLASS, 22 * global.ui_scale);
+ actor = this._inputSourcesManager.createFlagIcon(source, POPUP_MENU_ICON_STYLE_CLASS, 22);
}
if (actor == null) {
diff --git a/files/usr/share/cinnamon/applets/menu@cinnamon.org/appUtils.js b/files/usr/share/cinnamon/applets/menu@cinnamon.org/appUtils.js
index b5e532fb25..9eccefacbf 100644
--- a/files/usr/share/cinnamon/applets/menu@cinnamon.org/appUtils.js
+++ b/files/usr/share/cinnamon/applets/menu@cinnamon.org/appUtils.js
@@ -1,5 +1,8 @@
const Cinnamon = imports.gi.Cinnamon;
const CMenu = imports.gi.CMenu;
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const Util = imports.misc.util;
let appsys = Cinnamon.AppSystem.get_default();
@@ -115,3 +118,68 @@ function loadDirectory(dir, top_dir, apps) {
}
return has_entries;
}
+
+function _launchMintinstall(pkgName) {
+ Util.spawn(["mintinstall", "show", pkgName]);
+}
+
+// launch mintinstall on app page
+function launchMintinstallForApp(app) {
+ if (app.get_is_flatpak()) {
+ const pkgName = app.get_flatpak_app_id();
+ _launchMintinstall(pkgName);
+ } else {
+ const filePath = app.desktop_file_path;
+ if (!filePath) return;
+
+ const proc = Gio.Subprocess.new(
+ ['dpkg', '-S', filePath],
+ Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE
+ );
+ proc.communicate_utf8_async(null, null, (obj, res) => {
+ try {
+ let [success, stdout, stderr] = obj.communicate_utf8_finish(res);
+ if (success && stdout) {
+ const foundPkg = stdout.split(':')[0].trim();
+ _launchMintinstall(foundPkg);
+ }
+ } catch (e) {
+ global.logError("dpkg check failed: " + e.message);
+ }
+ });
+ }
+}
+
+function _launchPamac(pkgName) {
+ Util.spawn(["pamac-manager", `--details=${pkgName}`]);
+}
+
+// launch pamac-manager on app page
+function launchPamacForApp(app) {
+ if (app.get_is_flatpak()) {
+ // pamac-manager doesn't open on page of flatpak apps even if flatpak
+ // is enabled but let's launch it anyway so user can search for it.
+ const pkgName = app.get_flatpak_app_id();
+ _launchPamac(pkgName);
+ } else {
+ const filePath = app.desktop_file_path;
+ if (!filePath) return;
+
+ const proc = Gio.Subprocess.new(
+ ['pacman', '-Qqo', filePath],
+ Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE
+ );
+ proc.communicate_utf8_async(null, null, (obj, res) => {
+ try {
+ let [success, stdout, stderr] = obj.communicate_utf8_finish(res);
+ if (success && stdout) {
+ const foundPkg = stdout.trim();
+ _launchPamac(foundPkg);
+ }
+ } catch (e) {
+ global.logError("pacman check failed: " + e.message);
+ }
+ });
+ }
+}
+
diff --git a/files/usr/share/cinnamon/applets/menu@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/menu@cinnamon.org/applet.js
index 896470c305..74a2ed5120 100644
--- a/files/usr/share/cinnamon/applets/menu@cinnamon.org/applet.js
+++ b/files/usr/share/cinnamon/applets/menu@cinnamon.org/applet.js
@@ -14,7 +14,6 @@ const Gio = imports.gi.Gio;
const XApp = imports.gi.XApp;
const AccountsService = imports.gi.AccountsService;
const GnomeSession = imports.misc.gnomeSession;
-const ScreenSaver = imports.misc.screenSaver;
const FileUtils = imports.misc.fileUtils;
const Util = imports.misc.util;
const DND = imports.ui.dnd;
@@ -26,6 +25,7 @@ const Pango = imports.gi.Pango;
const SearchProviderManager = imports.ui.searchProviderManager;
const SignalManager = imports.misc.signalManager;
const Params = imports.misc.params;
+const Placeholder = imports.ui.placeholder;
const INITIAL_BUTTON_LOAD = 30;
@@ -34,7 +34,8 @@ const USER_DESKTOP_PATH = FileUtils.getUserDesktopDir();
const PRIVACY_SCHEMA = "org.cinnamon.desktop.privacy";
const REMEMBER_RECENT_KEY = "remember-recent-files";
-const AppUtils = require('./appUtils');
+const Me = imports.ui.extension.getCurrentExtension();
+const AppUtils = Me.imports.appUtils;
let appsys = Cinnamon.AppSystem.get_default();
@@ -135,7 +136,7 @@ class VisibleChildIterator {
* no-favorites "No favorite documents" button
* none Default type
* place PlaceButton
- * favorite PathButton
+ * favorite FavoriteDocumentButton
* recent PathButton
* recent-clear "Clear recent documents" button
* search-provider SearchProviderResultButton
@@ -346,11 +347,11 @@ class SimpleMenuItem {
}
}
-class ApplicationContextMenuItem extends PopupMenu.PopupBaseMenuItem {
- constructor(appButton, label, action, iconName) {
+class ContextMenuItem extends PopupMenu.PopupBaseMenuItem {
+ constructor(button, label, action, iconName) {
super({focusOnHover: false});
- this._appButton = appButton;
+ this._button = button;
this._action = action;
this.label = new St.Label({ text: label });
@@ -374,6 +375,12 @@ class ApplicationContextMenuItem extends PopupMenu.PopupBaseMenuItem {
this.actor.remove_accessible_state(Atk.StateType.FOCUSED);
});
}
+}
+
+class ApplicationContextMenuItem extends ContextMenuItem {
+ constructor(appButton, label, action, iconName) {
+ super(appButton, label, action, iconName);
+ }
activate (event) {
let closeMenu = true;
@@ -394,13 +401,13 @@ class ApplicationContextMenuItem extends PopupMenu.PopupBaseMenuItem {
let launcherApplet = Main.AppletManager.get_role_provider(Main.AppletManager.Roles.PANEL_LAUNCHER);
if (!launcherApplet)
return true;
- launcherApplet.acceptNewLauncher(this._appButton.app.get_id());
+ launcherApplet.acceptNewLauncher(this._button.app.get_id());
}
return false;
});
break;
case "add_to_desktop":
- let file = Gio.file_new_for_path(this._appButton.app.get_app_info().get_filename());
+ let file = Gio.file_new_for_path(this._button.app.get_app_info().get_filename());
let destFile = Gio.file_new_for_path(USER_DESKTOP_PATH+"/"+file.get_basename());
try{
file.copy(destFile, 0, null, function(){});
@@ -410,28 +417,33 @@ class ApplicationContextMenuItem extends PopupMenu.PopupBaseMenuItem {
}
break;
case "add_to_favorites":
- AppFavorites.getAppFavorites().addFavorite(this._appButton.app.get_id());
+ AppFavorites.getAppFavorites().addFavorite(this._button.app.get_id());
this.label.set_text(_("Remove from favorites"));
this.icon.icon_name = "xsi-starred";
this._action = "remove_from_favorites";
closeMenu = false;
break;
case "remove_from_favorites":
- AppFavorites.getAppFavorites().removeFavorite(this._appButton.app.get_id());
+ AppFavorites.getAppFavorites().removeFavorite(this._button.app.get_id());
this.label.set_text(_("Add to favorites"));
this.icon.icon_name = "xsi-non-starred";
this._action = "add_to_favorites";
closeMenu = false;
break;
- case "app_properties":
- Util.spawnCommandLine("cinnamon-desktop-editor -mlauncher -o" + GLib.shell_quote(this._appButton.app.get_app_info().get_filename()));
+ case "app_info":
+ if (this._appButton.applet._mintinstallAvailable) {
+ AppUtils.launchMintinstallForApp(this._appButton.app);
+ } else if (this._appButton.applet._pamacManagerAvailable) {
+ AppUtils.launchPamacForApp(this._appButton.app);
+ }
+ closeMenu = true;
break;
- case "uninstall":
- Util.spawnCommandLine("/usr/bin/cinnamon-remove-application '" + this._appButton.app.get_app_info().get_filename() + "'");
+ case "app_properties":
+ Util.spawnCommandLine("cinnamon-desktop-editor -mlauncher -o" + GLib.shell_quote(this._button.app.get_app_info().get_filename()));
break;
case "offload_launch":
try {
- this._appButton.app.launch_offloaded(0, [], -1);
+ this._button.app.launch_offloaded(0, [], -1);
} catch (e) {
logError(e, "Could not launch app with dedicated gpu: ");
}
@@ -439,16 +451,13 @@ class ApplicationContextMenuItem extends PopupMenu.PopupBaseMenuItem {
default:
if (this._action.startsWith("action_")) {
let action = this._action.substring(7);
- this._appButton.app.get_app_info().launch_action(action, global.create_app_launch_context());
+ this._button.app.get_app_info().launch_action(action, global.create_app_launch_context());
} else return true;
}
- if (closeMenu) {
- this._appButton.applet.toggleContextMenu(this._appButton);
- this._appButton.applet.menu.close();
- }
+ if (closeMenu)
+ this._button.applet.menu.close();
return false;
}
-
}
class GenericApplicationButton extends SimpleMenuItem {
@@ -530,13 +539,17 @@ class GenericApplicationButton extends SimpleMenuItem {
const appinfo = this.app.get_app_info();
- if (appinfo.get_filename() != null) {
- menuItem = new ApplicationContextMenuItem(this, _("Properties"), "app_properties", "xsi-document-properties-symbolic");
- menu.addMenuItem(menuItem);
+ if (this.applet._pamacManagerAvailable || this.applet._mintinstallAvailable) {
+ const filePath = this.app.desktop_file_path;
+ // Software managers usually only know of system installed apps.
+ if (!filePath.startsWith("/home/") && !filePath.includes("cinnamon-settings")) {
+ menuItem = new ApplicationContextMenuItem(this, _("App Info"), "app_info", "xsi-dialog-information-symbolic");
+ menu.addMenuItem(menuItem);
+ }
}
- if (this.applet._canUninstallApps) {
- menuItem = new ApplicationContextMenuItem(this, _("Uninstall"), "uninstall", "xsi-edit-delete");
+ if (appinfo.get_filename() != null) {
+ menuItem = new ApplicationContextMenuItem(this, _("Properties"), "app_properties", "xsi-document-properties-symbolic");
menu.addMenuItem(menuItem);
}
@@ -788,15 +801,79 @@ class RecentButton extends SimpleMenuItem {
}
}
+class PathContextMenuItem extends ContextMenuItem {
+ constructor(pathButton, label, action, iconName) {
+ super(pathButton, label, action, iconName);
+ }
+
+ activate(event) {
+ switch (this._action) {
+ case "open_containing_folder":
+ this._openContainingFolder();
+ this._button.applet.menu.close();
+ return false;
+ }
+ return true;
+ }
+
+ static _useDBus = true;
+
+ _openContainingFolder() {
+ if (!PathContextMenuItem._useDBus || !this._openContainingFolderViaDBus()) {
+ // Do not attempt to use DBus again once it's failed.
+ PathContextMenuItem._useDBus = false;
+ this._openContainingFolderViaMimeApp();
+ }
+ }
+
+ _openContainingFolderViaDBus() {
+ try {
+ Gio.DBus.session.call_sync(
+ "org.freedesktop.FileManager1",
+ "/org/freedesktop/FileManager1",
+ "org.freedesktop.FileManager1",
+ "ShowItems",
+ new GLib.Variant("(ass)", [
+ [this._button.uri],
+ global.get_pid().toString()
+ ]),
+ null,
+ Gio.DBusCallFlags.NONE,
+ 1000,
+ null
+ );
+ } catch (e) {
+ global.log(`Could not open containing folder via DBus: ${e}`);
+ return false;
+ }
+ return true;
+ }
+
+ _openContainingFolderViaMimeApp() {
+ let app = Gio.AppInfo.get_default_for_type("inode/directory", true);
+ if (app === null) {
+ log.logError(`Could not open containing folder via MIME app: No associated file manager found`);
+ return;
+ }
+ let file = Gio.file_new_for_uri(this._button.uri);
+ try {
+ app.launch([file.get_parent()], null);
+ } catch (e) {
+ global.logError(`Could not open containing folder via MIME app: ${e}`);
+ }
+ }
+}
+
class PathButton extends SimpleMenuItem {
- constructor(applet, type, name, uri, icon) {
+ constructor(applet, type, name, uri, mimeType, icon) {
super(applet, {
name: name,
description: shorten_path(uri, name),
type: type,
styleClass: 'appmenu-application-button',
- withMenu: false,
+ withMenu: true,
uri: uri,
+ mimeType: mimeType
});
this.icon = icon;
@@ -827,6 +904,55 @@ class PathButton extends SimpleMenuItem {
source.notify(notification);
}
}
+
+ populateMenu(menu) {
+ if (this.mimeType !== "inode/directory") {
+ let menuItem = new PathContextMenuItem(this, _("Open containing folder"), "open_containing_folder", "xsi-go-jump-symbolic");
+ menu.addMenuItem(menuItem);
+ }
+ }
+}
+
+class FavoriteDocumentContextMenuItem extends ContextMenuItem {
+ constructor(favDocButton, label, action, iconName) {
+ super(favDocButton, label, action, iconName);
+ }
+
+ activate(event) {
+ switch (this._action) {
+ case "remove_from_favorite_documents":
+ this._button._unfavorited = true;
+ // Do not refresh the favdoc menu during interaction, as it will destroy every menu item.
+ this._button.applet.deferRefreshMask |= RefreshFlags.FAV_DOC;
+ this._button.applet.closeContextMenu(true);
+ this._button.actor.hide();
+ XApp.Favorites.get_default().remove(this._button.uri);
+ return false;
+ }
+ return true;
+ }
+}
+
+class FavoriteDocumentButton extends PathButton {
+ constructor(applet, type, name, uri, mimeType, icon) {
+ super(applet, type, name, uri, mimeType, icon);
+
+ this._unfavorited = false;
+ this._signals.connect(this.actor, "show", () => {
+ if (this._unfavorited) {
+ this.actor.hide();
+ return Clutter.EVENT_STOP;
+ }
+ return Clutter.EVENT_PROPAGATE;
+ });
+ }
+
+ populateMenu(menu) {
+ let menuItem = new FavoriteDocumentContextMenuItem(this, _("Remove from favorites"), "remove_from_favorite_documents", "xsi-unfavorite-symbolic");
+ menu.addMenuItem(menuItem);
+
+ super.populateMenu(menu);
+ }
}
class CategoryButton extends SimpleMenuItem {
@@ -1229,7 +1355,8 @@ class CinnamonMenuApplet extends Applet.TextIconApplet {
this._activeActor = null;
this._knownApps = new Set(); // Used to keep track of apps that are already installed, so we can highlight newly installed ones
this._appsWereRefreshed = false;
- this._canUninstallApps = GLib.file_test("/usr/bin/cinnamon-remove-application", GLib.FileTest.EXISTS);
+ this._pamacManagerAvailable = GLib.find_program_in_path("pamac-manager");
+ this._mintinstallAvailable = GLib.find_program_in_path("mintinstall");
this.RecentManager = DocInfo.getDocManager();
this.privacy_settings = new Gio.Settings( {schema_id: PRIVACY_SCHEMA} );
this.noRecentDocuments = true;
@@ -1250,13 +1377,12 @@ class CinnamonMenuApplet extends Applet.TextIconApplet {
this.orderDirty = false;
this._session = new GnomeSession.SessionManager();
- this._screenSaverProxy = new ScreenSaver.ScreenSaverProxy();
-
// We shouldn't need to call refreshAll() here... since we get a "icon-theme-changed" signal when CSD starts.
// The reason we do is in case the Cinnamon icon theme is the same as the one specified in GTK itself (in .config)
// In that particular case we get no signal at all.
this.refreshId = 0;
this.refreshMask = REFRESH_ALL_MASK;
+ this.deferRefreshMask = 0;
this._doRefresh();
this.set_show_label_in_vertical_panels(false);
@@ -1307,7 +1433,7 @@ class CinnamonMenuApplet extends Applet.TextIconApplet {
_doRefresh() {
this.refreshId = 0;
- if (this.refreshMask === 0)
+ if ((this.refreshMask &= ~this.deferRefreshMask) === 0)
return;
let m = this.refreshMask;
@@ -1446,6 +1572,10 @@ class CinnamonMenuApplet extends Applet.TextIconApplet {
if (this.searchActive) {
this.resetSearch();
}
+ if (this.deferRefreshMask !== 0) {
+ this.queueRefresh(this.deferRefreshMask);
+ this.deferRefreshMask = 0;
+ }
this.hoveredCategory = null;
this.hoveredApp = null;
@@ -1613,7 +1743,8 @@ class CinnamonMenuApplet extends Applet.TextIconApplet {
button.populateMenu(this.contextMenu);
}
- this.contextMenu.toggle();
+ if (this.contextMenu.numMenuItems !== 0)
+ this.contextMenu.toggle();
}
_navigateContextMenu(button, symbol, ctrlKey) {
@@ -2223,7 +2354,7 @@ class CinnamonMenuApplet extends Applet.TextIconApplet {
this.noRecentDocuments = false;
recents.forEach( info => {
let icon = info.createIcon(this.applicationIconSize);
- let button = new PathButton(this, 'recent', info.name, info.uri, icon);
+ let button = new PathButton(this, 'recent', info.name, info.uri, info.mimeType, icon);
this._recentButtons.push(button);
this.applicationsBox.add_actor(button.actor);
button.actor.visible = this.menu.isOpen && this.lastSelectedCategory === "recent";
@@ -2246,14 +2377,19 @@ class CinnamonMenuApplet extends Applet.TextIconApplet {
button.actor.visible = this.menu.isOpen && this.lastSelectedCategory === "recent";
} else {
this.noRecentDocuments = true;
- let button = new SimpleMenuItem(this, { name: _("No recent documents"),
- type: 'no-recent',
- styleClass: 'appmenu-application-button',
- reactive: false,
- activatable: false });
- button.addLabel(button.name, 'appmenu-application-button-label');
+ let button = new SimpleMenuItem(this, {
+ type: 'no-recent',
+ reactive:false,
+ });
+ let placeHolder = new Placeholder.Placeholder({
+ icon_name: 'xsi-document-open-recent-symbolic',
+ title: _('No Recent Documents'),
+ });
+ button.actor.y_expand = true;
+ button.actor.y_align = Clutter.ActorAlign.CENTER;
+ button.actor.add_child(placeHolder);
this._recentButtons.push(button);
- this.applicationsBox.add_actor(button.actor);
+ this.applicationsBox.add_child(button.actor);
button.actor.visible = this.menu.isOpen && this.lastSelectedCategory === "recent";
}
}
@@ -2287,21 +2423,27 @@ class CinnamonMenuApplet extends Applet.TextIconApplet {
gicon: Gio.content_type_get_icon(info.cached_mimetype),
icon_size: this.applicationIconSize
});
- let button = new PathButton(this, 'favorite', info.display_name, info.uri, icon);
+ let button = new FavoriteDocumentButton(this, 'favorite', info.display_name, info.uri, info.cached_mimetype, icon);
this._favoriteDocButtons.push(button);
this.applicationsBox.add_actor(button.actor);
button.actor.visible = this.menu.isOpen && this.lastSelectedCategory === "favorite";
});
}
else {
- let button = new SimpleMenuItem(this, { name: _("No favorite documents"),
- type: 'no-favorites',
- styleClass: 'appmenu-application-button',
- reactive: false,
- activatable: false });
- button.addLabel(button.name, 'appmenu-application-button-label');
+ let button = new SimpleMenuItem(this, {
+ type: 'no-favorites',
+ reactive: false,
+ });
+ let placeHolder = new Placeholder.Placeholder({
+ icon_name: 'xsi-user-favorites-symbolic',
+ title: _('No Favorite Documents'),
+ description: _("Files you add to Favorites in your file manager will be shown here")
+ });
+ button.actor.y_expand = true;
+ button.actor.y_align = Clutter.ActorAlign.CENTER;
+ button.actor.add_child(placeHolder);
this._favoriteDocButtons.push(button);
- this.applicationsBox.add_actor(button.actor);
+ this.applicationsBox.add_child(button.actor);
button.actor.visible = this.menu.isOpen && this.lastSelectedCategory === "favorite";
}
}
@@ -2382,20 +2524,7 @@ class CinnamonMenuApplet extends Applet.TextIconApplet {
button.activate = () => {
this.menu.close();
-
- let screensaver_settings = new Gio.Settings({ schema_id: "org.cinnamon.desktop.screensaver" });
- let screensaver_dialog = GLib.find_program_in_path("cinnamon-screensaver-command");
- if (screensaver_dialog) {
- if (screensaver_settings.get_boolean("ask-for-away-message")) {
- Util.spawnCommandLine("cinnamon-screensaver-lock-dialog");
- }
- else {
- Util.spawnCommandLine("cinnamon-screensaver-command --lock");
- }
- }
- else {
- this._screenSaverProxy.LockRemote("");
- }
+ Main.lockScreen(true);
};
this.systemBox.add(button.actor, { y_align: St.Align.MIDDLE, y_fill: false });
diff --git a/files/usr/share/cinnamon/applets/menu@cinnamon.org/settings-schema.json b/files/usr/share/cinnamon/applets/menu@cinnamon.org/settings-schema.json
index c9fa8c290e..cea7360fd1 100644
--- a/files/usr/share/cinnamon/applets/menu@cinnamon.org/settings-schema.json
+++ b/files/usr/share/cinnamon/applets/menu@cinnamon.org/settings-schema.json
@@ -5,7 +5,7 @@
"content" : {
"type" : "page",
"title" : "Content",
- "sections" : ["content-places", "content-content"]
+ "sections" : ["content-content", "content-places"]
},
"appearance" : {
"type" : "page",
@@ -39,15 +39,16 @@
"title" : "Panel",
"keys" : ["menu-custom", "menu-icon", "menu-icon-size", "menu-label"]
},
- "content-places" : {
- "type" : "section",
- "title" : "Places",
- "keys" : ["show-home", "show-desktop", "show-documents", "show-downloads", "show-music", "show-pictures", "show-videos", "show-bookmarks"]
- },
"content-content" : {
"type" : "section",
"title" : "Content",
"keys" : ["show-sidebar", "show-avatar", "show-favorites", "show-recents", "menu-editor-button"]
+ },
+ "content-places" : {
+ "dependency" : "show-sidebar",
+ "type" : "section",
+ "title" : "Places",
+ "keys" : ["show-home", "show-desktop", "show-documents", "show-downloads", "show-music", "show-pictures", "show-videos", "show-bookmarks"]
}
},
"overlay-key" : {
@@ -112,6 +113,7 @@
"description" : "Sidebar"
},
"show-avatar" : {
+ "dependency" : "show-sidebar",
"type" : "switch",
"default" : true,
"description" : "Avatar"
@@ -185,6 +187,7 @@
"dependency" : "show-sidebar"
},
"sidebar-max-width": {
+ "dependency" : "show-sidebar",
"type": "spinbutton",
"default": 180,
"min": 130,
diff --git a/files/usr/share/cinnamon/applets/power@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/power@cinnamon.org/applet.js
index dd63c0c81a..7721819fd2 100644
--- a/files/usr/share/cinnamon/applets/power@cinnamon.org/applet.js
+++ b/files/usr/share/cinnamon/applets/power@cinnamon.org/applet.js
@@ -3,6 +3,7 @@ const Clutter = imports.gi.Clutter;
const Gio = imports.gi.Gio;
const Interfaces = imports.misc.interfaces
const Lang = imports.lang;
+const PowerUtils = imports.misc.powerUtils;
const St = imports.gi.St;
const Tooltips = imports.ui.tooltips;
const UPowerGlib = imports.gi.UPowerGlib;
@@ -18,11 +19,10 @@ const CSD_BACKLIGHT_NOT_SUPPORTED_CODE = 1;
const PANEL_EDIT_MODE_KEY = "panel-edit-mode";
const {
- DeviceKind: UPDeviceKind,
- DeviceLevel: UPDeviceLevel,
- DeviceState: UPDeviceState,
- Device: UPDevice
-} = UPowerGlib
+ UPDeviceKind,
+ UPDeviceLevel,
+ UPDeviceState
+} = PowerUtils;
const POWER_PROFILES = {
"power-saver": _("Power Saver"),
@@ -30,136 +30,6 @@ const POWER_PROFILES = {
"performance": _("Performance")
};
-function deviceLevelToString(level) {
- switch (level) {
- case UPDeviceLevel.FULL:
- return _("Battery full");
- case UPDeviceLevel.HIGH:
- return _("Battery almost full");
- case UPDeviceLevel.NORMAL:
- return _("Battery good");
- case UPDeviceLevel.LOW:
- return _("Low battery");
- case UPDeviceLevel.CRITICAL:
- return _("Critically low battery");
- default:
- return _("Unknown");
- }
-}
-
-function deviceKindToString(kind) {
- switch (kind) {
- case UPDeviceKind.LINE_POWER:
- return _("AC adapter");
- case UPDeviceKind.BATTERY:
- return _("Laptop battery");
- case UPDeviceKind.UPS:
- return _("UPS");
- case UPDeviceKind.MONITOR:
- return _("Monitor");
- case UPDeviceKind.MOUSE:
- return _("Mouse");
- case UPDeviceKind.KEYBOARD:
- return _("Keyboard");
- case UPDeviceKind.PDA:
- return _("PDA");
- case UPDeviceKind.PHONE:
- return _("Cell phone");
- case UPDeviceKind.MEDIA_PLAYER:
- return _("Media player");
- case UPDeviceKind.TABLET:
- return _("Tablet");
- case UPDeviceKind.COMPUTER:
- return _("Computer");
- case UPDeviceKind.GAMING_INPUT:
- return _("Gaming input");
- case UPDeviceKind.PEN:
- return _("Pen");
- case UPDeviceKind.TOUCHPAD:
- return _("Touchpad");
- case UPDeviceKind.MODEM:
- return _("Modem");
- case UPDeviceKind.NETWORK:
- return _("Network");
- case UPDeviceKind.HEADSET:
- return _("Headset");
- case UPDeviceKind.SPEAKERS:
- return _("Speakers");
- case UPDeviceKind.HEADPHONES:
- return _("Headphones");
- case UPDeviceKind.VIDEO:
- return _("Video");
- case UPDeviceKind.OTHER_AUDIO:
- return _("Audio device");
- case UPDeviceKind.REMOTE_CONTROL:
- return _("Remote control");
- case UPDeviceKind.PRINTER:
- return _("Printer");
- case UPDeviceKind.SCANNER:
- return _("Scanner");
- case UPDeviceKind.CAMERA:
- return _("Camera");
- case UPDeviceKind.WEARABLE:
- return _("Wearable");
- case UPDeviceKind.TOY:
- return _("Toy");
- case UPDeviceKind.BLUETOOTH_GENERIC:
- return _("Bluetooth device");
- default: {
- try {
- return UPDevice.kind_to_string(kind).replaceAll("-", " ").capitalize();
- } catch {
- return _("Unknown");
- }
- }
- }
-}
-
-function deviceKindToIcon(kind, icon) {
- switch (kind) {
- case UPDeviceKind.MONITOR:
- return ("xsi-video-display");
- case UPDeviceKind.MOUSE:
- return ("xsi-input-mouse");
- case UPDeviceKind.KEYBOARD:
- return ("xsi-input-keyboard");
- case UPDeviceKind.PHONE:
- case UPDeviceKind.MEDIA_PLAYER:
- return ("xsi-phone-apple-iphone");
- case UPDeviceKind.TABLET:
- return ("xsi-input-tablet");
- case UPDeviceKind.COMPUTER:
- return ("xsi-computer");
- case UPDeviceKind.GAMING_INPUT:
- return ("xsi-input-gaming");
- case UPDeviceKind.TOUCHPAD:
- return ("xsi-input-touchpad");
- case UPDeviceKind.HEADSET:
- return ("xsi-audio-headset");
- case UPDeviceKind.SPEAKERS:
- return ("xsi-audio-speakers");
- case UPDeviceKind.HEADPHONES:
- return ("xsi-audio-headphones");
- case UPDeviceKind.PRINTER:
- return ("xsi-printer");
- case UPDeviceKind.SCANNER:
- return ("xsi-scanner");
- case UPDeviceKind.CAMERA:
- return ("xsi-camera-photo");
- default:
- if (icon) {
- return icon;
- }
- else {
- return ("xsi-battery-level-100");
- }
- }
-}
-
-function reportsPreciseLevels(battery_level) {
- return battery_level == UPDeviceLevel.NONE;
-}
-
class DeviceItem extends PopupMenu.PopupBaseMenuItem {
constructor(device, status, aliases) {
super({ reactive: false });
@@ -169,7 +39,7 @@ class DeviceItem extends PopupMenu.PopupBaseMenuItem {
this._box = new St.BoxLayout({ style_class: 'popup-device-menu-item' });
this._vbox = new St.BoxLayout({ style_class: 'popup-device-menu-item', vertical: true });
- let description = deviceKindToString(device_kind);
+ let description = PowerUtils.deviceKindToString(device_kind);
if (vendor != "" || model != "") {
description = "%s %s".format(vendor, model);
}
@@ -191,14 +61,14 @@ class DeviceItem extends PopupMenu.PopupBaseMenuItem {
let statusLabel = null;
if (battery_level == UPDeviceLevel.NONE) {
- this.label = new St.Label({ text: "%s %d%%".format(description, Math.round(percentage)) });
+ this.label = new St.Label({ text: "%d%% %s".format(Math.round(percentage), description) });
statusLabel = new St.Label({ text: "%s".format(status), style_class: 'popup-inactive-menu-item' });
} else {
this.label = new St.Label({ text: "%s".format(description) });
- statusLabel = new St.Label({ text: "%s".format(deviceLevelToString(battery_level)), style_class: 'popup-inactive-menu-item' });
+ statusLabel = new St.Label({ text: "%s".format(PowerUtils.deviceLevelToString(battery_level)), style_class: 'popup-inactive-menu-item' });
}
- let device_icon = deviceKindToIcon(device_kind, icon);
+ let device_icon = PowerUtils.deviceKindToIcon(device_kind, icon);
if (device_icon == icon) {
this._icon = new St.Icon({ gicon: Gio.icon_new_for_string(icon), icon_type: St.IconType.SYMBOLIC, style_class: 'popup-menu-icon' });
}
@@ -692,12 +562,12 @@ class CinnamonPowerApplet extends Applet.TextIconApplet {
if (state == UPDeviceState.UNKNOWN)
continue;
- if (reportsPreciseLevels(battery_level)) {
+ if (PowerUtils.reportsPreciseLevels(battery_level)) {
// Devices that give accurate % charge will return this for battery level.
pct_support_count++;
}
- let stats = "%s (%d%%)".format(deviceKindToString(device_kind), percentage);
+ let stats = "%s (%d%%)".format(PowerUtils.deviceKindToString(device_kind), percentage);
devices_stats.push(stats);
_devices.push(devices[i]);
@@ -749,7 +619,7 @@ class CinnamonPowerApplet extends Applet.TextIconApplet {
let [, , , , , percentage, , battery_level, seconds] = this._devices[i];
// Skip devices without accurate reporting
- if (!reportsPreciseLevels(battery_level)) {
+ if (!PowerUtils.reportsPreciseLevels(battery_level)) {
continue;
}
diff --git a/files/usr/share/cinnamon/applets/sound@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/sound@cinnamon.org/applet.js
index 7e1f51e312..0fa8c48852 100644
--- a/files/usr/share/cinnamon/applets/sound@cinnamon.org/applet.js
+++ b/files/usr/share/cinnamon/applets/sound@cinnamon.org/applet.js
@@ -2,7 +2,6 @@ const Applet = imports.ui.applet;
const Lang = imports.lang;
const Mainloop = imports.mainloop;
const Gio = imports.gi.Gio;
-const Interfaces = imports.misc.interfaces;
const Util = imports.misc.util;
const Cinnamon = imports.gi.Cinnamon;
const Clutter = imports.gi.Clutter;
@@ -15,9 +14,8 @@ const Main = imports.ui.main;
const Settings = imports.ui.settings;
const Slider = imports.ui.slider;
const Pango = imports.gi.Pango;
+const MprisPlayerModule = imports.misc.mprisPlayer;
-const MEDIA_PLAYER_2_PATH = "/org/mpris/MediaPlayer2";
-const MEDIA_PLAYER_2_NAME = "org.mpris.MediaPlayer2";
const MEDIA_PLAYER_2_PLAYER_NAME = "org.mpris.MediaPlayer2.Player";
// how long to show the output icon when volume is adjusted during media playback.
@@ -483,36 +481,36 @@ class StreamMenuSection extends PopupMenu.PopupMenuSection {
}
class Player extends PopupMenu.PopupMenuSection {
- constructor(applet, busname, owner) {
+ constructor(applet, mprisPlayer) {
super();
- this._owner = owner;
- this._busName = busname;
+ this._mprisPlayer = mprisPlayer;
+ this._owner = mprisPlayer.getOwner();
+ this._busName = mprisPlayer.getBusName();
this._applet = applet;
- // We'll update this later with a proper name
- this._name = this._busName;
+ // Get name from MprisPlayer
+ this._name = mprisPlayer.getIdentity() || this._busName;
- let asyncReadyCb = (proxy, error, property) => {
- if (error)
- log(error);
- else {
- this[property] = proxy;
- this._dbus_acquired();
- }
- };
-
- Interfaces.getDBusProxyWithOwnerAsync(MEDIA_PLAYER_2_NAME,
- this._busName,
- (p, e) => asyncReadyCb(p, e, '_mediaServer'));
-
- Interfaces.getDBusProxyWithOwnerAsync(MEDIA_PLAYER_2_PLAYER_NAME,
- this._busName,
- (p, e) => asyncReadyCb(p, e, '_mediaServerPlayer'));
-
- Interfaces.getDBusPropertiesAsync(this._busName,
- MEDIA_PLAYER_2_PATH,
- (p, e) => asyncReadyCb(p, e, '_prop'));
+ // Get proxies from MprisPlayer (shared module handles creation)
+ this._mediaServer = mprisPlayer.getMediaServerProxy();
+ this._mediaServerPlayer = mprisPlayer.getMediaServerPlayerProxy();
+ this._prop = mprisPlayer.getPropertiesProxy();
+ // If MprisPlayer is already ready, initialize immediately
+ if (mprisPlayer.isReady()) {
+ this._dbus_acquired();
+ } else {
+ // Wait for proxies to be ready
+ this._readyId = mprisPlayer.connect('ready', () => {
+ this._mediaServer = mprisPlayer.getMediaServerProxy();
+ this._mediaServerPlayer = mprisPlayer.getMediaServerPlayerProxy();
+ this._prop = mprisPlayer.getPropertiesProxy();
+ this._name = mprisPlayer.getIdentity() || this._busName;
+ mprisPlayer.disconnect(this._readyId);
+ this._readyId = 0;
+ this._dbus_acquired();
+ });
+ }
}
_dbus_acquired() {
@@ -890,7 +888,8 @@ class Player extends PopupMenu.PopupMenuSection {
}
else {
this._cover_path = cover_path;
- this._cover_load_handle = St.TextureCache.get_default().load_image_from_file_async(cover_path, 300, 300, this._on_cover_loaded.bind(this));
+ const cover_size = 300 * global.ui_scale;
+ this._cover_load_handle = St.TextureCache.get_default().load_image_from_file_async(cover_path, cover_size, cover_size, this._on_cover_loaded.bind(this));
}
}
@@ -904,7 +903,7 @@ class Player extends PopupMenu.PopupMenuSection {
// Make sure any oddly-shaped album art doesn't affect the height of the applet popup
// (and move the player controls as a result).
- actor.margin_bottom = 300 - actor.height;
+ actor.margin_bottom = (300 * global.ui_scale) - actor.height;
this.cover = actor;
this.coverBox.add_actor(this.cover);
@@ -918,10 +917,18 @@ class Player extends PopupMenu.PopupMenuSection {
}
destroy() {
- this._seeker.destroy();
- if (this._prop)
+ if (this._readyId && this._mprisPlayer) {
+ this._mprisPlayer.disconnect(this._readyId);
+ this._readyId = 0;
+ }
+
+ if (this._seeker)
+ this._seeker.destroy();
+ if (this._prop && this._propChangedId)
this._prop.disconnectSignal(this._propChangedId);
+ this._mprisPlayer = null;
+
PopupMenu.PopupMenuSection.prototype.destroy.call(this);
}
}
@@ -993,40 +1000,20 @@ class CinnamonSoundApplet extends Applet.TextIconApplet {
this._playerItems = [];
this._activePlayer = null;
- Interfaces.getDBusAsync((proxy, error) => {
- if (error) {
- // ?? what else should we do if we fail completely here?
- throw error;
- }
-
- this._dbus = proxy;
-
- // player DBus name pattern
- let name_regex = /^org\.mpris\.MediaPlayer2\./;
- // load players
- this._dbus.ListNamesRemote((names) => {
- for (let n in names[0]) {
- let name = names[0][n];
- if (name_regex.test(name))
- this._dbus.GetNameOwnerRemote(name, (owner) => this._addPlayer(name, owner[0]));
- }
- });
-
- // watch players
- this._ownerChangedId = this._dbus.connectSignal('NameOwnerChanged',
- (proxy, sender, [name, old_owner, new_owner]) => {
- if (name_regex.test(name)) {
- if (new_owner && !old_owner)
- this._addPlayer(name, new_owner);
- else if (old_owner && !new_owner)
- this._removePlayer(name, old_owner);
- else
- this._changePlayerOwner(name, old_owner, new_owner);
- }
- }
- );
+ // Use shared MPRIS module for player discovery
+ this._mprisManager = MprisPlayerModule.getMprisPlayerManager();
+ this._playerAddedId = this._mprisManager.connect('player-added', (manager, mprisPlayer) => {
+ this._addPlayer(mprisPlayer);
+ });
+ this._playerRemovedId = this._mprisManager.connect('player-removed', (manager, busName, owner) => {
+ this._removePlayer(busName, owner);
});
+ // Add any players that already exist
+ for (let mprisPlayer of this._mprisManager.getPlayers()) {
+ this._addPlayer(mprisPlayer);
+ }
+
this._control = new Cvc.MixerControl({ name: 'Cinnamon Volume Control' });
this._control.connect('state-changed', (...args) => this._onControlStateChanged(...args));
@@ -1150,7 +1137,10 @@ class CinnamonSoundApplet extends Applet.TextIconApplet {
this._iconTimeoutId = 0;
}
- this._dbus.disconnectSignal(this._ownerChangedId);
+ if (this._mprisManager) {
+ this._mprisManager.disconnect(this._playerAddedId);
+ this._mprisManager.disconnect(this._playerRemovedId);
+ }
for(let i in this._players)
this._players[i].destroy();
@@ -1405,7 +1395,10 @@ class CinnamonSoundApplet extends Applet.TextIconApplet {
/^org\.mpris\.MediaPlayer2\.vlc-\d+$/.test(busName);
}
- _addPlayer(busName, owner) {
+ _addPlayer(mprisPlayer) {
+ let owner = mprisPlayer.getOwner();
+ let busName = mprisPlayer.getBusName();
+
if (this._players[owner]) {
let prevName = this._players[owner]._busName;
// HAVE: ADDING: ACTION:
@@ -1418,12 +1411,12 @@ class CinnamonSoundApplet extends Applet.TextIconApplet {
else
return;
} else if (owner) {
- let player = new Player(this, busName, owner);
+ let player = new Player(this, mprisPlayer);
// Add the player to the list of active players in GUI.
- // We don't have the org.mpris.MediaPlayer2 interface set up at this point,
- // add the player's busName as a placeholder until we can get its Identity.
- let item = new PopupMenu.PopupMenuItem(busName);
+ // Use the identity from MprisPlayer if available, otherwise busName as placeholder
+ let displayName = mprisPlayer.getIdentity() || busName;
+ let item = new PopupMenu.PopupMenuItem(displayName);
item.activate = () => this._switchPlayer(player._owner);
this._chooseActivePlayerItem.menu.addMenuItem(item);
@@ -1481,16 +1474,6 @@ class CinnamonSoundApplet extends Applet.TextIconApplet {
}
}
- _changePlayerOwner(busName, oldOwner, newOwner) {
- if (this._players[oldOwner] && busName == this._players[oldOwner]._busName) {
- this._players[newOwner] = this._players[oldOwner];
- this._players[newOwner]._owner = newOwner;
- delete this._players[oldOwner];
- if (this._activePlayer == oldOwner)
- this._activePlayer = newOwner;
- }
- }
-
//will be called by an instance of #Player
passDesktopEntry(entry) {
//do we know already this player?
diff --git a/files/usr/share/cinnamon/applets/user@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/user@cinnamon.org/applet.js
index ef4fe9f558..132ecbfe7b 100644
--- a/files/usr/share/cinnamon/applets/user@cinnamon.org/applet.js
+++ b/files/usr/share/cinnamon/applets/user@cinnamon.org/applet.js
@@ -3,11 +3,11 @@ const Lang = imports.lang;
const St = imports.gi.St;
const PopupMenu = imports.ui.popupMenu;
const Util = imports.misc.util;
+const Main = imports.ui.main;
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
const AccountsService = imports.gi.AccountsService;
const GnomeSession = imports.misc.gnomeSession;
-const ScreenSaver = imports.misc.screenSaver;
const Settings = imports.ui.settings;
const UserWidget = imports.ui.userWidget;
@@ -27,7 +27,6 @@ class CinnamonUserApplet extends Applet.TextApplet {
this._panel_avatar = null;
this._session = new GnomeSession.SessionManager();
- this._screenSaverProxy = new ScreenSaver.ScreenSaverProxy();
this.settings = new Settings.AppletSettings(this, "user@cinnamon.org", instance_id);
this.menuManager = new PopupMenu.PopupMenuManager(this);
@@ -79,50 +78,17 @@ class CinnamonUserApplet extends Applet.TextApplet {
item = new PopupMenu.PopupIconMenuItem(_("Lock Screen"), "xsi-lock-screen", St.IconType.SYMBOLIC);
item.connect('activate', Lang.bind(this, function() {
- let screensaver_settings = new Gio.Settings({ schema_id: "org.cinnamon.desktop.screensaver" });
- let screensaver_dialog = Gio.file_new_for_path("/usr/bin/cinnamon-screensaver-command");
- if (screensaver_dialog.query_exists(null)) {
- if (screensaver_settings.get_boolean("ask-for-away-message")) {
- Util.spawnCommandLine("cinnamon-screensaver-lock-dialog");
- }
- else {
- Util.spawnCommandLine("cinnamon-screensaver-command --lock");
- }
- }
- else {
- this._screenSaverProxy.LockRemote();
- }
+ Main.lockScreen(true);
}));
this.menu.addMenuItem(item);
- let lockdown_settings = new Gio.Settings({ schema_id: 'org.cinnamon.desktop.lockdown' });
- if (!lockdown_settings.get_boolean('disable-user-switching')) {
- if (GLib.getenv("XDG_SEAT_PATH")) {
- // LightDM
- item = new PopupMenu.PopupIconMenuItem(_("Switch User"), "xsi-switch-user", St.IconType.SYMBOLIC);
- item.connect('activate', Lang.bind(this, function() {
- Util.spawnCommandLine("cinnamon-screensaver-command --lock");
- Util.spawnCommandLine("dm-tool switch-to-greeter");
- }));
- this.menu.addMenuItem(item);
- }
- else if (GLib.file_test("/usr/bin/mdmflexiserver", GLib.FileTest.EXISTS)) {
- // MDM
- item = new PopupMenu.PopupIconMenuItem(_("Switch User"), "xsi-switch-user", St.IconType.SYMBOLIC);
- item.connect('activate', Lang.bind(this, function() {
- Util.spawnCommandLine("mdmflexiserver");
- }));
- this.menu.addMenuItem(item);
- }
- else if (GLib.file_test("/usr/bin/gdmflexiserver", GLib.FileTest.EXISTS)) {
- // GDM
- item = new PopupMenu.PopupIconMenuItem(_("Switch User"), "xsi-switch-user", St.IconType.SYMBOLIC);
- item.connect('activate', Lang.bind(this, function() {
- Util.spawnCommandLine("cinnamon-screensaver-command --lock");
- Util.spawnCommandLine("gdmflexiserver");
- }));
- this.menu.addMenuItem(item);
- }
+ if (!Main.lockdownSettings.get_boolean('disable-user-switching')) {
+ item = new PopupMenu.PopupIconMenuItem(_("Switch User"), "xsi-switch-user", St.IconType.SYMBOLIC);
+ item.connect('activate', Lang.bind(this, function() {
+ Main.lockScreen(false);
+ Util.switchToGreeter();
+ }));
+ this.menu.addMenuItem(item);
}
item = new PopupMenu.PopupIconMenuItem(_("Log Out..."), "xsi-log-out", St.IconType.SYMBOLIC);
diff --git a/files/usr/share/cinnamon/applets/xapp-status@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/xapp-status@cinnamon.org/applet.js
index 417d17b19e..a85cd6e1bf 100644
--- a/files/usr/share/cinnamon/applets/xapp-status@cinnamon.org/applet.js
+++ b/files/usr/share/cinnamon/applets/xapp-status@cinnamon.org/applet.js
@@ -218,12 +218,13 @@ class XAppStatusIcon {
// Assume symbolic icons would always be square/suitable for an StIcon.
if (iconName.includes("/") && type != St.IconType.SYMBOLIC) {
+ const scaledIconSize = this.iconSize * global.ui_scale;
this.icon_loader_handle = St.TextureCache.get_default().load_image_from_file_async(
iconName,
/* If top/bottom panel, allow the image to expand horizontally,
* otherwise, restrict it to a square (but keep aspect ratio.) */
- this.actor.vertical ? this.iconSize : -1,
- this.iconSize,
+ this.actor.vertical ? scaledIconSize : -1,
+ scaledIconSize,
(...args)=>this._onImageLoaded(...args)
);
diff --git a/files/usr/share/cinnamon/cinnamon-desktop-editor/cinnamon-desktop-editor.py b/files/usr/share/cinnamon/cinnamon-desktop-editor/cinnamon-desktop-editor.py
index 412dca8d24..ba879b4a2b 100755
--- a/files/usr/share/cinnamon/cinnamon-desktop-editor/cinnamon-desktop-editor.py
+++ b/files/usr/share/cinnamon/cinnamon-desktop-editor/cinnamon-desktop-editor.py
@@ -18,8 +18,8 @@
sys.path.insert(0, '/usr/share/cinnamon/cinnamon-menu-editor')
from cme import util
-sys.path.insert(0, '/usr/share/cinnamon/cinnamon-settings/bin')
-import JsonSettingsWidgets
+sys.path.insert(0, '/usr/share/cinnamon/cinnamon-settings')
+from bin import JsonSettingsWidgets
# i18n
gettext.install("cinnamon", "/usr/share/locale")
diff --git a/files/usr/share/cinnamon/cinnamon-screensaver-command/cinnamon-screensaver-command.py b/files/usr/share/cinnamon/cinnamon-screensaver-command/cinnamon-screensaver-command.py
new file mode 100755
index 0000000000..3fac424c11
--- /dev/null
+++ b/files/usr/share/cinnamon/cinnamon-screensaver-command/cinnamon-screensaver-command.py
@@ -0,0 +1,200 @@
+#!/usr/bin/python3
+
+from gi.repository import GLib, Gio
+import sys
+import signal
+import shlex
+import argparse
+import gettext
+from subprocess import Popen, DEVNULL
+from enum import IntEnum
+
+signal.signal(signal.SIGINT, signal.SIG_DFL)
+gettext.install("cinnamon", "/usr/share/locale")
+
+# DBus interface constants
+SS_SERVICE = "org.cinnamon.ScreenSaver"
+SS_PATH = "/org/cinnamon/ScreenSaver"
+SS_INTERFACE = "org.cinnamon.ScreenSaver"
+
+class Action(IntEnum):
+ EXIT = 1
+ QUERY = 2
+ TIME = 3
+ LOCK = 4
+ ACTIVATE = 5
+ DEACTIVATE = 6
+ VERSION = 7
+
+class ScreensaverCommand:
+ """
+ Standalone executable for controlling the screensaver via DBus.
+ Supports both internal (Main.screenShield) and external (cinnamon-screensaver) modes.
+ """
+ def __init__(self, mainloop):
+ self.mainloop = mainloop
+ self.proxy = None
+
+ parser = argparse.ArgumentParser(description='Cinnamon Screensaver Command')
+ parser.add_argument('--exit', '-e', dest="action_id", action='store_const', const=Action.EXIT,
+ help=_('Causes the screensaver to exit gracefully'))
+ parser.add_argument('--query', '-q', dest="action_id", action='store_const', const=Action.QUERY,
+ help=_('Query the state of the screensaver'))
+ parser.add_argument('--time', '-t', dest="action_id", action='store_const', const=Action.TIME,
+ help=_('Query the length of time the screensaver has been active'))
+ parser.add_argument('--lock', '-l', dest="action_id", action='store_const', const=Action.LOCK,
+ help=_('Tells the running screensaver process to lock the screen immediately'))
+ parser.add_argument('--activate', '-a', dest="action_id", action='store_const', const=Action.ACTIVATE,
+ help=_('Turn the screensaver on (blank the screen)'))
+ parser.add_argument('--deactivate', '-d', dest="action_id", action='store_const', const=Action.DEACTIVATE,
+ help=_('If the screensaver is active then deactivate it (un-blank the screen)'))
+ parser.add_argument('--version', '-V', dest="action_id", action='store_const', const=Action.VERSION,
+ help=_('Version of this application'))
+ parser.add_argument('--away-message', '-m', dest="message", action='store', default="",
+ help=_('Message to be displayed in lock screen'))
+ args = parser.parse_args()
+
+ if not args.action_id:
+ parser.print_help()
+ quit()
+
+ if args.action_id == Action.VERSION:
+ # Get version from cinnamon
+ try:
+ version_proxy = Gio.DBusProxy.new_for_bus_sync(
+ Gio.BusType.SESSION,
+ Gio.DBusProxyFlags.NONE,
+ None,
+ 'org.Cinnamon',
+ '/org/Cinnamon',
+ 'org.Cinnamon',
+ None
+ )
+ version = version_proxy.get_cached_property('CinnamonVersion')
+ if version:
+ print("cinnamon-screensaver-command (Cinnamon %s)" % version.unpack())
+ else:
+ print("cinnamon-screensaver-command (Cinnamon version unknown)")
+ except:
+ print("cinnamon-screensaver-command")
+ quit()
+
+ self.action_id = args.action_id
+ self.message = args.message
+
+ ss_settings = Gio.Settings.new("org.cinnamon.desktop.screensaver")
+ custom_saver = ss_settings.get_string("custom-screensaver-command").strip()
+ if custom_saver:
+ self._handle_custom_saver(custom_saver)
+ quit()
+
+ # Create DBus proxy
+ Gio.DBusProxy.new_for_bus(
+ Gio.BusType.SESSION,
+ Gio.DBusProxyFlags.NONE,
+ None,
+ SS_SERVICE,
+ SS_PATH,
+ SS_INTERFACE,
+ None,
+ self._on_proxy_ready
+ )
+
+ def _handle_custom_saver(self, custom_saver):
+ if self.action_id in (Action.LOCK, Action.ACTIVATE):
+ try:
+ Popen(shlex.split(custom_saver), stdin=DEVNULL)
+ except OSError as e:
+ print("Error %d running %s: %s" % (e.errno, custom_saver, e.strerror))
+ else:
+ print("Action not supported with custom screensaver.")
+
+ def _on_proxy_ready(self, source, result):
+ try:
+ self.proxy = Gio.DBusProxy.new_for_bus_finish(result)
+ self.perform_action()
+ except GLib.Error as e:
+ print("Can't connect to screensaver: %s" % e.message)
+ self.mainloop.quit()
+
+ def perform_action(self):
+ try:
+ if self.action_id == Action.EXIT:
+ self.proxy.call_sync(
+ 'Quit',
+ None,
+ Gio.DBusCallFlags.NONE,
+ -1,
+ None
+ )
+
+ elif self.action_id == Action.QUERY:
+ result = self.proxy.call_sync(
+ 'GetActive',
+ None,
+ Gio.DBusCallFlags.NONE,
+ -1,
+ None
+ )
+ if result:
+ is_active = result.unpack()[0]
+ if is_active:
+ print(_("The screensaver is active\n"))
+ else:
+ print(_("The screensaver is inactive\n"))
+
+ elif self.action_id == Action.TIME:
+ result = self.proxy.call_sync(
+ 'GetActiveTime',
+ None,
+ Gio.DBusCallFlags.NONE,
+ -1,
+ None
+ )
+ if result:
+ time = result.unpack()[0]
+ if time == 0:
+ print(_("The screensaver is not currently active.\n"))
+ else:
+ print(gettext.ngettext(
+ "The screensaver has been active for %d second.\n",
+ "The screensaver has been active for %d seconds.\n",
+ time
+ ) % time)
+
+ elif self.action_id == Action.LOCK:
+ self.proxy.call_sync(
+ 'Lock',
+ GLib.Variant('(s)', (self.message,)),
+ Gio.DBusCallFlags.NONE,
+ -1,
+ None
+ )
+
+ elif self.action_id == Action.ACTIVATE:
+ self.proxy.call_sync(
+ 'SetActive',
+ GLib.Variant('(b)', (True,)),
+ Gio.DBusCallFlags.NONE,
+ -1,
+ None
+ )
+
+ elif self.action_id == Action.DEACTIVATE:
+ self.proxy.call_sync(
+ 'SetActive',
+ GLib.Variant('(b)', (False,)),
+ Gio.DBusCallFlags.NONE,
+ -1,
+ None
+ )
+
+ except GLib.Error as e:
+ print("Error executing command: %s" % e.message)
+
+ self.mainloop.quit()
+
+if __name__ == "__main__":
+ ml = GLib.MainLoop.new(None, True)
+ main = ScreensaverCommand(ml)
+ ml.run()
diff --git a/files/usr/share/cinnamon/cinnamon-screensaver-lock-dialog/cinnamon-screensaver-lock-dialog.py b/files/usr/share/cinnamon/cinnamon-screensaver-lock-dialog/cinnamon-screensaver-lock-dialog.py
deleted file mode 100755
index f9c47ebb01..0000000000
--- a/files/usr/share/cinnamon/cinnamon-screensaver-lock-dialog/cinnamon-screensaver-lock-dialog.py
+++ /dev/null
@@ -1,68 +0,0 @@
-#!/usr/bin/python3
-
-import os
-import subprocess
-import gettext
-import pwd
-from setproctitle import setproctitle
-
-import gi
-gi.require_version("Gtk", "3.0")
-gi.require_version("XApp", "1.0")
-from gi.repository import Gtk, XApp
-
-# i18n
-gettext.install("cinnamon", "/usr/share/locale")
-
-
-class MainWindow:
-
- """ Create the UI """
-
- def __init__(self):
-
- user_id = os.getuid()
- username = pwd.getpwuid(user_id).pw_name
- home_dir = pwd.getpwuid(user_id).pw_dir
-
- self.builder = Gtk.Builder()
- self.builder.set_translation_domain('cinnamon') # let it translate!
- self.builder.add_from_file("/usr/share/cinnamon/cinnamon-screensaver-lock-dialog/cinnamon-screensaver-lock-dialog.ui")
-
- self.window = self.builder.get_object("main_dialog")
- self.button_cancel = self.builder.get_object("button_cancel")
- self.button_ok = self.builder.get_object("button_ok")
- self.entry = self.builder.get_object("entry_away_message")
- self.image = self.builder.get_object("image_face")
-
- self.window.set_title(_("Screen Locker"))
- XApp.set_window_icon_name(self.window, "cs-screensaver")
-
- self.builder.get_object("label_description").set_markup("%s" % _("Please type an away message for the lock screen"))
-
- if os.path.exists("%s/.face" % home_dir):
- self.image.set_from_file("%s/.face" % home_dir)
- else:
- self.image.set_from_icon_name("cs-screensaver", Gtk.IconSize.DIALOG)
-
- self.window.connect("destroy", Gtk.main_quit)
- self.button_cancel.connect("clicked", Gtk.main_quit)
- self.button_ok.connect('clicked', self.lock_screen)
- self.entry.connect('activate', self.lock_screen)
-
- self.builder.get_object("dialog-action_area1").set_focus_chain((self.button_ok, self.button_cancel))
-
- self.window.show()
-
- def lock_screen(self, data):
- message = self.entry.get_text()
- if message != "":
- subprocess.call(["cinnamon-screensaver-command", "--lock", "--away-message", self.entry.get_text()])
- else:
- subprocess.call(["cinnamon-screensaver-command", "--lock"])
- Gtk.main_quit()
-
-if __name__ == "__main__":
- setproctitle("cinnamon-screensaver-lock-dialog")
- MainWindow()
- Gtk.main()
diff --git a/files/usr/share/cinnamon/cinnamon-screensaver-lock-dialog/cinnamon-screensaver-lock-dialog.ui b/files/usr/share/cinnamon/cinnamon-screensaver-lock-dialog/cinnamon-screensaver-lock-dialog.ui
deleted file mode 100644
index 3d61817cd4..0000000000
--- a/files/usr/share/cinnamon/cinnamon-screensaver-lock-dialog/cinnamon-screensaver-lock-dialog.ui
+++ /dev/null
@@ -1,134 +0,0 @@
-
-
-
-
-
-
diff --git a/files/usr/share/cinnamon/cinnamon-settings/bin/AddKeyboardLayout.py b/files/usr/share/cinnamon/cinnamon-settings/bin/AddKeyboardLayout.py
index baca3656f9..71587e4ef1 100644
--- a/files/usr/share/cinnamon/cinnamon-settings/bin/AddKeyboardLayout.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/bin/AddKeyboardLayout.py
@@ -12,7 +12,7 @@
gi.require_version('Pango', '1.0')
from gi.repository import GLib, Gio, Gtk, GObject, CinnamonDesktop, IBus, Pango
-from SettingsWidgets import Keybinding
+from bin.SettingsWidgets import Keybinding
from xapp.SettingsWidgets import SettingsPage
from xapp.GSettingsWidgets import PXGSettingsBackend, GSettingsSwitch
diff --git a/files/usr/share/cinnamon/cinnamon-settings/bin/ExtensionCore.py b/files/usr/share/cinnamon/cinnamon-settings/bin/ExtensionCore.py
index 56a4fc93c9..3b32c14e67 100644
--- a/files/usr/share/cinnamon/cinnamon-settings/bin/ExtensionCore.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/bin/ExtensionCore.py
@@ -15,7 +15,7 @@
from gi.repository import Gio, Gtk, Gdk, GdkPixbuf, GLib
from xapp.SettingsWidgets import SettingsPage, SettingsWidget, SettingsLabel
-from Spices import ThreadedTaskManager
+from bin.Spices import ThreadedTaskManager
home = os.path.expanduser('~')
@@ -582,6 +582,9 @@ def update_button_states(self, *args):
if self.collection_type == 'action' and hasattr(row, 'disabled_about'):
self.about_button.set_sensitive(not row.disabled_about)
+ # Disable "Disable all" button when no extensions are installed
+ self.restore_button.set_sensitive(len(self.extension_rows) > 0)
+
def add_instance(self, *args):
extension_row = self.list_box.get_selected_row()
self.enable_extension(extension_row.uuid, extension_row.name, extension_row.version_supported)
@@ -659,6 +662,7 @@ def load_extensions(self, *args):
print(f"Failed to load extension {uuid}: {msg}")
self.list_box.show_all()
+ self.update_button_states()
def update_status(self, *args):
for row in self.extension_rows:
diff --git a/files/usr/share/cinnamon/cinnamon-settings/bin/InputSources.py b/files/usr/share/cinnamon/cinnamon-settings/bin/InputSources.py
index 76dc502fb8..4cac543758 100644
--- a/files/usr/share/cinnamon/cinnamon-settings/bin/InputSources.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/bin/InputSources.py
@@ -10,11 +10,11 @@
gi.require_version('IBus', '1.0')
from gi.repository import GLib, Gio, Gtk, GObject, CinnamonDesktop, IBus
-from SettingsWidgets import GSettingsKeybinding
+from bin.SettingsWidgets import GSettingsKeybinding
from xapp.SettingsWidgets import SettingsPage
from xapp.GSettingsWidgets import PXGSettingsBackend, GSettingsSwitch
-import AddKeyboardLayout
+from bin import AddKeyboardLayout
MAX_LAYOUTS_PER_GROUP = 4
diff --git a/files/usr/share/cinnamon/cinnamon-settings/bin/JsonSettingsWidgets.py b/files/usr/share/cinnamon/cinnamon-settings/bin/JsonSettingsWidgets.py
index 687f007aa0..2ed7443115 100644
--- a/files/usr/share/cinnamon/cinnamon-settings/bin/JsonSettingsWidgets.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/bin/JsonSettingsWidgets.py
@@ -2,10 +2,10 @@
from gi.repository import Gio
from xapp.SettingsWidgets import *
-from SettingsWidgets import SoundFileChooser, DateChooser, TimeChooser, Keybinding
+from bin.SettingsWidgets import SoundFileChooser, DateChooser, TimeChooser, Keybinding
from xapp.GSettingsWidgets import CAN_BACKEND as px_can_backend
-from SettingsWidgets import CAN_BACKEND as c_can_backend
-from TreeListWidgets import List
+from bin.SettingsWidgets import CAN_BACKEND as c_can_backend
+from bin.TreeListWidgets import List
import os
import collections
import json
diff --git a/files/usr/share/cinnamon/cinnamon-settings/bin/SettingsWidgets.py b/files/usr/share/cinnamon/cinnamon-settings/bin/SettingsWidgets.py
index 47fadb7e5d..efa4efe643 100755
--- a/files/usr/share/cinnamon/cinnamon-settings/bin/SettingsWidgets.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/bin/SettingsWidgets.py
@@ -9,11 +9,11 @@
from xapp.SettingsWidgets import SettingsWidget, SettingsLabel
from xapp.GSettingsWidgets import PXGSettingsBackend
-from ChooserButtonWidgets import DateChooserButton, TimeChooserButton
-from KeybindingWidgets import ButtonKeybinding
+from bin.ChooserButtonWidgets import DateChooserButton, TimeChooserButton
+from bin.KeybindingWidgets import ButtonKeybinding
from bin import util
-import KeybindingTable
+from bin import KeybindingTable
settings_objects = {}
diff --git a/files/usr/share/cinnamon/cinnamon-settings/bin/Spices.py b/files/usr/share/cinnamon/cinnamon-settings/bin/Spices.py
index e5d4747dcc..c3a66d9fe1 100644
--- a/files/usr/share/cinnamon/cinnamon-settings/bin/Spices.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/bin/Spices.py
@@ -409,7 +409,7 @@ def _url_retrieve(self, url, outfd, reporthook, binary):
# Like the one in urllib. Unlike urllib.retrieve url_retrieve
# can be interrupted. KeyboardInterrupt exception is raised when
# interrupted.
- import proxygsettings
+ from bin import proxygsettings
import requests
count = 0
diff --git a/files/usr/share/cinnamon/cinnamon-settings/bin/TreeListWidgets.py b/files/usr/share/cinnamon/cinnamon-settings/bin/TreeListWidgets.py
index e5668cae4d..99f13dae1e 100644
--- a/files/usr/share/cinnamon/cinnamon-settings/bin/TreeListWidgets.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/bin/TreeListWidgets.py
@@ -4,7 +4,7 @@
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from xapp.SettingsWidgets import *
-from SettingsWidgets import SoundFileChooser, Keybinding
+from bin.SettingsWidgets import SoundFileChooser, Keybinding
VARIABLE_TYPE_MAP = {
"string" : str,
diff --git a/files/usr/share/cinnamon/cinnamon-settings/bin/test-add-layout b/files/usr/share/cinnamon/cinnamon-settings/bin/test-add-layout
index 5ed13cf8b2..6def2e238f 100755
--- a/files/usr/share/cinnamon/cinnamon-settings/bin/test-add-layout
+++ b/files/usr/share/cinnamon/cinnamon-settings/bin/test-add-layout
@@ -1,6 +1,10 @@
#!/usr/bin/python3
-import AddKeyboardLayout
+import os
+import sys
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+from bin import AddKeyboardLayout
import gettext
if __name__ == "__main__":
diff --git a/files/usr/share/cinnamon/cinnamon-settings/cinnamon-settings.py b/files/usr/share/cinnamon/cinnamon-settings/cinnamon-settings.py
index fe22b66619..6fd06093e2 100755
--- a/files/usr/share/cinnamon/cinnamon-settings/cinnamon-settings.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/cinnamon-settings.py
@@ -27,9 +27,7 @@
PYTHON_CS_MODULE_PATH = os.path.join(CURRENT_PATH, "modules")
PYTHON_CS_MODULE_GLOB = os.path.join(PYTHON_CS_MODULE_PATH, "cs_*.py")
PYTHON_CS_MODULES = [Path(file).stem for file in glob.glob(PYTHON_CS_MODULE_GLOB)]
-BIN_PATH = os.path.join(CURRENT_PATH, "bin")
sys.path.append(PYTHON_CS_MODULE_PATH)
-sys.path.append(BIN_PATH)
from bin import capi
from bin import proxygsettings
from bin import SettingsWidgets
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_accessibility.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_accessibility.py
index f01c3a4835..89842da115 100755
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_accessibility.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_accessibility.py
@@ -5,7 +5,7 @@
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
-from SettingsWidgets import SidePage, GSettingsDependencySwitch, DependencyCheckInstallButton, GSettingsSoundFileChooser
+from bin.SettingsWidgets import SidePage, GSettingsDependencySwitch, DependencyCheckInstallButton, GSettingsSoundFileChooser
from xapp.GSettingsWidgets import *
DPI_FACTOR_LARGE = 1.25
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_actions.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_actions.py
index bcd7d1e51a..d25d5156ba 100644
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_actions.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_actions.py
@@ -3,9 +3,9 @@
from pathlib import Path
import sys
-from ExtensionCore import ManageSpicesPage, DownloadSpicesPage
-from Spices import Spice_Harvester
-from SettingsWidgets import SidePage
+from bin.ExtensionCore import ManageSpicesPage, DownloadSpicesPage
+from bin.Spices import Spice_Harvester
+from bin.SettingsWidgets import SidePage
from xapp.GSettingsWidgets import *
from gi.repository import GLib
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_applets.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_applets.py
index 7865795f17..0b8af7ed2a 100644
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_applets.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_applets.py
@@ -1,10 +1,10 @@
#!/usr/bin/python3
import sys
-from ExtensionCore import ManageSpicesPage, DownloadSpicesPage
-from SettingsWidgets import SidePage
+from bin.ExtensionCore import ManageSpicesPage, DownloadSpicesPage
+from bin.SettingsWidgets import SidePage
from xapp.SettingsWidgets import SettingsStack
-from Spices import Spice_Harvester
+from bin.Spices import Spice_Harvester
from gi.repository import GLib, Gtk, Gdk
import config
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_backgrounds.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_backgrounds.py
index 75f0b5751f..e2463baae4 100755
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_backgrounds.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_backgrounds.py
@@ -18,7 +18,7 @@
gi.require_version("Gtk", "3.0")
from gi.repository import Gio, Gtk, Gdk, GdkPixbuf, Pango, GLib
-from SettingsWidgets import SidePage
+from bin.SettingsWidgets import SidePage
from xapp.GSettingsWidgets import *
gettext.install("cinnamon", "/usr/share/locale")
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_calendar.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_calendar.py
index c688bd4402..0f2cd6f28f 100755
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_calendar.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_calendar.py
@@ -1,7 +1,7 @@
#!/usr/bin/python3
-from ChooserButtonWidgets import DateChooserButton, TimeChooserButton
-from SettingsWidgets import SidePage
+from bin.ChooserButtonWidgets import DateChooserButton, TimeChooserButton
+from bin.SettingsWidgets import SidePage
from xapp.GSettingsWidgets import *
from zoneinfo import ZoneInfo, available_timezones
import gi
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_default.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_default.py
index e35791f202..d71a15c17f 100755
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_default.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_default.py
@@ -2,7 +2,7 @@
import os
-from SettingsWidgets import SidePage
+from bin.SettingsWidgets import SidePage
from xapp.GSettingsWidgets import *
import gi
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_desklets.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_desklets.py
index 713d948301..50281765c5 100644
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_desklets.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_desklets.py
@@ -1,8 +1,8 @@
#!/usr/bin/python3
-from ExtensionCore import ManageSpicesPage, DownloadSpicesPage
-from Spices import Spice_Harvester
-from SettingsWidgets import SidePage
+from bin.ExtensionCore import ManageSpicesPage, DownloadSpicesPage
+from bin.Spices import Spice_Harvester
+from bin.SettingsWidgets import SidePage
from xapp.GSettingsWidgets import *
from gi.repository import GLib, Gtk
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_desktop.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_desktop.py
index 48084e5b0f..f0b750371f 100755
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_desktop.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_desktop.py
@@ -1,6 +1,6 @@
#!/usr/bin/python3
-from SettingsWidgets import SidePage
+from bin.SettingsWidgets import SidePage
from xapp.GSettingsWidgets import *
DESKTOP_SCHEMA = "org.nemo.desktop"
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_display.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_display.py
index e4b510164c..921c320336 100644
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_display.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_display.py
@@ -1,6 +1,6 @@
#!/usr/bin/python3
-from SettingsWidgets import SidePage
+from bin.SettingsWidgets import SidePage
from xapp.GSettingsWidgets import *
FRACTIONAL_ENABLE_OPTIONS = ["scale-monitor-framebuffer", "x11-randr-fractional-scaling"]
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_effects.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_effects.py
index a2c6405822..7652a9df6e 100755
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_effects.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_effects.py
@@ -1,6 +1,6 @@
#!/usr/bin/python3
-from SettingsWidgets import SidePage
+from bin.SettingsWidgets import SidePage
from xapp.GSettingsWidgets import *
SCHEMA = "org.cinnamon"
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_extensions.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_extensions.py
index 293f8ff3a0..d5544dcd2a 100755
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_extensions.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_extensions.py
@@ -1,9 +1,9 @@
#!/usr/bin/python3
-from ExtensionCore import ManageSpicesPage, DownloadSpicesPage
-from SettingsWidgets import SidePage
+from bin.ExtensionCore import ManageSpicesPage, DownloadSpicesPage
+from bin.SettingsWidgets import SidePage
from xapp.SettingsWidgets import SettingsStack
-from Spices import Spice_Harvester
+from bin.Spices import Spice_Harvester
from gi.repository import GLib
class Module:
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_fonts.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_fonts.py
index 46c9535595..3872ca6ef4 100755
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_fonts.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_fonts.py
@@ -4,7 +4,7 @@
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
-from SettingsWidgets import SidePage
+from bin.SettingsWidgets import SidePage
from xapp.GSettingsWidgets import *
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_general.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_general.py
index 55970a99bc..aeae69c476 100755
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_general.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_general.py
@@ -1,6 +1,6 @@
#!/usr/bin/python3
-from SettingsWidgets import SidePage
+from bin.SettingsWidgets import SidePage
from bin import util
from xapp.GSettingsWidgets import *
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_gestures.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_gestures.py
index e3a913be62..82ca69465f 100644
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_gestures.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_gestures.py
@@ -6,7 +6,7 @@
gi.require_version('Gtk', '3.0')
from gi.repository import Gio, Gtk
-from SettingsWidgets import SidePage, SettingsWidget
+from bin.SettingsWidgets import SidePage, SettingsWidget
from xapp.GSettingsWidgets import *
SCHEMA = "org.cinnamon.gestures"
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_hotcorner.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_hotcorner.py
index a0708063c9..4cd46cbf7a 100755
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_hotcorner.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_hotcorner.py
@@ -5,7 +5,7 @@
from gi.repository import Gio, GLib
-from SettingsWidgets import SidePage
+from bin.SettingsWidgets import SidePage
from xapp.GSettingsWidgets import *
_270_DEG = 270.0 * (math.pi/180.0)
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_info.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_info.py
index 154098b1f0..d4279c6c1d 100755
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_info.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_info.py
@@ -11,7 +11,7 @@
from gi.repository import GdkPixbuf
-from SettingsWidgets import SidePage
+from bin.SettingsWidgets import SidePage
from bin import util
from xapp.GSettingsWidgets import *
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_keyboard.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_keyboard.py
index baeb473149..54a4ad7f88 100755
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_keyboard.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_keyboard.py
@@ -12,8 +12,8 @@
gi.require_version("Gtk", "3.0")
from gi.repository import Gdk, Gio, Gtk
-from KeybindingWidgets import ButtonKeybinding, CellRendererKeybinding
-from SettingsWidgets import SidePage, Keybinding
+from bin.KeybindingWidgets import ButtonKeybinding, CellRendererKeybinding
+from bin.SettingsWidgets import SidePage, Keybinding
from bin import util
from bin import InputSources
from bin import XkbSettings
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_mouse.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_mouse.py
index 7dba9479a7..a256e9f5da 100755
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_mouse.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_mouse.py
@@ -5,7 +5,7 @@
gi.require_version("CDesktopEnums", "3.0")
from gi.repository import Gtk, Gdk, GLib, CDesktopEnums
-from SettingsWidgets import SidePage
+from bin.SettingsWidgets import SidePage
from xapp.GSettingsWidgets import *
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_nightlight.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_nightlight.py
index 6c184494ee..d4ef890d34 100644
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_nightlight.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_nightlight.py
@@ -1,6 +1,6 @@
#!/usr/bin/python3
-from SettingsWidgets import SidePage
+from bin.SettingsWidgets import SidePage
from bin import util
from xapp.GSettingsWidgets import *
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_notifications.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_notifications.py
index df38d801b7..785b2e5f2e 100755
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_notifications.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_notifications.py
@@ -4,7 +4,7 @@
gi.require_version('Notify', '0.7')
from gi.repository import Gio, Notify
-from SettingsWidgets import SidePage
+from bin.SettingsWidgets import SidePage
from xapp.GSettingsWidgets import *
content = """
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_panel.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_panel.py
index a084025507..0abfafdc6e 100755
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_panel.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_panel.py
@@ -6,7 +6,7 @@
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
-from SettingsWidgets import SidePage
+from bin.SettingsWidgets import SidePage
from xapp.GSettingsWidgets import *
import config
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_power.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_power.py
index 4c81833f20..81d5087217 100755
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_power.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_power.py
@@ -4,7 +4,7 @@
gi.require_version('UPowerGlib', '1.0')
from gi.repository import UPowerGlib
-from SettingsWidgets import SidePage
+from bin.SettingsWidgets import SidePage
from xapp.GSettingsWidgets import *
POWER_BUTTON_OPTIONS = [
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_privacy.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_privacy.py
index 26693b2300..c7982f7289 100755
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_privacy.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_privacy.py
@@ -3,7 +3,7 @@
gi.require_version('Gtk', '3.0')
from gi.repository import Gio, Gtk
-from SettingsWidgets import SidePage
+from bin.SettingsWidgets import SidePage
from xapp.GSettingsWidgets import GSettingsSwitch, SettingsLabel, SettingsPage, SettingsRevealer, SettingsWidget, Switch
PRIVACY_SCHEMA = "org.cinnamon.desktop.privacy"
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_screensaver.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_screensaver.py
index 4c2eb26b73..1dfb7fc22b 100755
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_screensaver.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_screensaver.py
@@ -6,7 +6,7 @@
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
-from SettingsWidgets import SidePage
+from bin.SettingsWidgets import SidePage
from xapp.GSettingsWidgets import *
LOCK_DELAY_OPTIONS = [
@@ -121,12 +121,6 @@ def on_module_selected(self):
widget.pack_start(button, True, True, 0)
settings.add_reveal_row(widget, schema, "use-custom-format")
- widget = GSettingsFontButton(_("Time Font"), "org.cinnamon.desktop.screensaver", "font-time", size_group=size_group)
- settings.add_row(widget)
-
- widget = GSettingsFontButton(_("Date Font"), "org.cinnamon.desktop.screensaver", "font-date", size_group=size_group)
- settings.add_row(widget)
-
settings = page.add_section(_("Away message"))
widget = GSettingsEntry(_("Show this message when the screen is locked"), schema, "default-message")
@@ -134,8 +128,6 @@ def on_module_selected(self):
widget.set_tooltip_text(_("This is the default message displayed on your lock screen"))
settings.add_row(widget)
- settings.add_row(GSettingsFontButton(_("Font"), "org.cinnamon.desktop.screensaver", "font-message"))
-
widget = GSettingsSwitch(_("Ask for a custom message when locking the screen from the menu"), schema, "ask-for-away-message")
widget.set_tooltip_text(_("This option allows you to type a message each time you lock the screen from the menu"))
settings.add_row(widget)
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_sound.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_sound.py
index 0c5da57336..bf6386ef14 100755
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_sound.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_sound.py
@@ -4,7 +4,7 @@
gi.require_version('Cvc', '1.0')
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Cvc, Gdk, GdkPixbuf, Gio, Pango
-from SettingsWidgets import SidePage, GSettingsSoundFileChooser
+from bin.SettingsWidgets import SidePage, GSettingsSoundFileChooser
from xapp.GSettingsWidgets import *
from bin import util
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_startup.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_startup.py
index fb7e47f3f9..7e013681dc 100755
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_startup.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_startup.py
@@ -9,7 +9,7 @@
gi.require_version('Gtk', '3.0')
from gi.repository import Gio, Gtk, Gdk, GdkPixbuf, GLib, Pango
-from SettingsWidgets import SidePage
+from bin.SettingsWidgets import SidePage
from xapp.GSettingsWidgets import *
try:
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_themes.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_themes.py
index f7c47c6c98..8c98a3780f 100755
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_themes.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_themes.py
@@ -7,11 +7,11 @@
from gi.repository import Gtk, GdkPixbuf
from xapp.GSettingsWidgets import *
-from CinnamonGtkSettings import CssRange, CssOverrideSwitch, GtkSettingsSwitch, PreviewWidget, Gtk2ScrollbarSizeEditor
-from SettingsWidgets import LabelRow, SidePage, walk_directories
-from ChooserButtonWidgets import PictureChooserButton
-from ExtensionCore import DownloadSpicesPage
-from Spices import Spice_Harvester
+from bin.CinnamonGtkSettings import CssRange, CssOverrideSwitch, GtkSettingsSwitch, PreviewWidget, Gtk2ScrollbarSizeEditor
+from bin.SettingsWidgets import LabelRow, SidePage, walk_directories
+from bin.ChooserButtonWidgets import PictureChooserButton
+from bin.ExtensionCore import DownloadSpicesPage
+from bin.Spices import Spice_Harvester
from pathlib import Path
import config
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_thunderbolt.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_thunderbolt.py
index f611247b13..12ce8602ff 100644
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_thunderbolt.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_thunderbolt.py
@@ -8,7 +8,7 @@
gi.require_version("GLib", "2.0")
from gi.repository import Gtk, Gio, GLib
-from SettingsWidgets import SidePage
+from bin.SettingsWidgets import SidePage
from xapp.SettingsWidgets import SettingsStack, SettingsPage, SettingsSection, SettingsWidget, SettingsLabel
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_user.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_user.py
index f24012d85c..080d32a99c 100755
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_user.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_user.py
@@ -19,8 +19,8 @@
gi.require_version('AccountsService', '1.0')
from gi.repository import AccountsService, GLib, GdkPixbuf, XApp
-from SettingsWidgets import SidePage
-from ChooserButtonWidgets import PictureChooserButton
+from bin.SettingsWidgets import SidePage
+from bin.ChooserButtonWidgets import PictureChooserButton
from xapp.GSettingsWidgets import *
class PasswordError(Exception):
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_windows.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_windows.py
index cf60c959b5..e1ac378a1d 100755
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_windows.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_windows.py
@@ -5,7 +5,7 @@
gi.require_version('CDesktopEnums', '3.0')
from gi.repository import Gio, Gtk, CDesktopEnums
-from SettingsWidgets import SidePage
+from bin.SettingsWidgets import SidePage
from xapp.GSettingsWidgets import *
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_workspaces.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_workspaces.py
index 7acc3f03c1..cbe3a2e35c 100755
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_workspaces.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_workspaces.py
@@ -1,6 +1,6 @@
#!/usr/bin/python3
-from SettingsWidgets import SidePage
+from bin.SettingsWidgets import SidePage
from xapp.GSettingsWidgets import *
diff --git a/files/usr/share/cinnamon/cinnamon-settings/xlet-settings.py b/files/usr/share/cinnamon/cinnamon-settings/xlet-settings.py
index 4ea84404c8..41aad31130 100755
--- a/files/usr/share/cinnamon/cinnamon-settings/xlet-settings.py
+++ b/files/usr/share/cinnamon/cinnamon-settings/xlet-settings.py
@@ -18,8 +18,8 @@
import traceback
from pathlib import Path
-from JsonSettingsWidgets import *
-from ExtensionCore import find_extension_subdir
+from bin.JsonSettingsWidgets import *
+from bin.ExtensionCore import find_extension_subdir
from gi.repository import Gtk, Gio, XApp, GLib
# i18n
diff --git a/generate_cs_module_desktop_files.py b/generate_cs_module_desktop_files.py
index cc8e1cc265..3d5f6fd804 100755
--- a/generate_cs_module_desktop_files.py
+++ b/generate_cs_module_desktop_files.py
@@ -18,7 +18,6 @@
try:
sys.path.append('files/usr/share/cinnamon/cinnamon-settings')
sys.path.append('files/usr/share/cinnamon/cinnamon-settings/modules')
- sys.path.append('files/usr/share/cinnamon/cinnamon-settings/bin')
mod_files = glob.glob('files/usr/share/cinnamon/cinnamon-settings/modules/*.py')
mod_files.sort()
if len(mod_files) == 0:
diff --git a/js/misc/authClient.js b/js/misc/authClient.js
new file mode 100644
index 0000000000..aa819b4a3a
--- /dev/null
+++ b/js/misc/authClient.js
@@ -0,0 +1,238 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const ByteArray = imports.byteArray;
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const Signals = imports.signals;
+
+const Config = imports.misc.config;
+
+const SIGTERM = 15;
+
+var AuthClient = class {
+ constructor() {
+ this.reset();
+ }
+
+ reset() {
+ this.initialized = false;
+ this.cancellable = null;
+ this.proc = null;
+ this.in_pipe = null;
+ this.out_pipe = null;
+ }
+
+ initialize() {
+ if (this.initialized)
+ return true;
+
+ this.cancellable = new Gio.Cancellable();
+
+ try {
+ let helper_path = GLib.build_filenamev([Config.LIBEXECDIR, 'cinnamon-screensaver-pam-helper']);
+
+ let argv = [helper_path];
+ let flags = Gio.SubprocessFlags.STDIN_PIPE | Gio.SubprocessFlags.STDOUT_PIPE;
+
+ if (global.settings.get_boolean('debug-screensaver')) {
+ argv.push('--debug');
+ } else {
+ flags |= Gio.SubprocessFlags.STDERR_SILENCE;
+ }
+
+ this.proc = Gio.Subprocess.new(argv, flags);
+ } catch (e) {
+ global.logError('authClient: error starting cinnamon-screensaver-pam-helper: ' + e.message);
+ return false;
+ }
+
+ this.proc.wait_check_async(this.cancellable, this._onProcCompleted.bind(this));
+
+ this.out_pipe = this.proc.get_stdout_pipe();
+ this.in_pipe = this.proc.get_stdin_pipe();
+
+ this.initialized = true;
+
+ this._readMessages();
+
+ return true;
+ }
+
+ cancel() {
+ this._endProc();
+ }
+
+ _endProc() {
+ if (this.cancellable == null)
+ return;
+
+ this.cancellable.cancel();
+ if (this.proc != null) {
+ this.proc.send_signal(SIGTERM);
+ }
+
+ this.reset();
+ }
+
+ _onProcCompleted(proc, res) {
+ try {
+ proc.wait_check_finish(res);
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
+ global.logError('helper process did not exit cleanly: ' + e.message);
+ }
+ }
+
+ let pipe = proc.get_stdin_pipe();
+ if (pipe != null) {
+ try {
+ pipe.close(null);
+ } catch (e) {
+ // Ignore pipe close errors
+ }
+ }
+
+ pipe = proc.get_stdout_pipe();
+ if (pipe != null) {
+ try {
+ pipe.close(null);
+ } catch (e) {
+ // Ignore pipe close errors
+ }
+ }
+
+ // Don't just reset - if another proc has been started we don't want to interfere.
+ if (this.proc == proc) {
+ this.reset();
+ }
+ }
+
+ sendPassword(password) {
+ if (!this.initialized)
+ return;
+
+ if (this.cancellable == null || this.cancellable.is_cancelled())
+ return;
+
+ try {
+ let bytes = ByteArray.fromString(password + '\n');
+ let gbytes = GLib.Bytes.new(bytes);
+ this.in_pipe.write_bytes(gbytes, this.cancellable);
+ this.in_pipe.flush(this.cancellable);
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
+ global.logError('Error writing to pam helper: ' + e.message);
+ }
+ }
+ }
+
+ _readMessages() {
+ if (this.cancellable == null || this.cancellable.is_cancelled())
+ return;
+
+ this.out_pipe.read_bytes_async(1024, GLib.PRIORITY_DEFAULT, this.cancellable, this._onMessageFromHelper.bind(this));
+ }
+
+ _onMessageFromHelper(pipe, res) {
+ if (this.cancellable == null || this.cancellable.is_cancelled())
+ return;
+
+ let terminate = false;
+
+ try {
+ let bytes_read = pipe.read_bytes_finish(res);
+
+ if (!bytes_read || bytes_read.get_size() === 0) {
+ global.logWarning('authClient: PAM helper pipe returned no data, helper may have died');
+ GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
+ this.emit('auth-cancel');
+ return GLib.SOURCE_REMOVE;
+ });
+ this._endProc();
+ return;
+ }
+
+ if (bytes_read.get_size() > 0) {
+ let raw_string = ByteArray.toString(bytes_read.toArray());
+ let lines = raw_string.split('\n');
+
+ for (let i = 0; i < lines.length; i++) {
+ let output = lines[i];
+ if (output.length > 0) {
+ if (global.settings.get_boolean('debug-screensaver'))
+ global.log(`authClient: received: '${output}'`);
+
+ if (output === 'CS_PAM_AUTH_FAILURE') {
+ GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
+ this.emit('auth-failure');
+ return GLib.SOURCE_REMOVE;
+ });
+ } else if (output === 'CS_PAM_AUTH_SUCCESS') {
+ GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
+ this.emit('auth-success');
+ return GLib.SOURCE_REMOVE;
+ });
+ terminate = true;
+ } else if (output === 'CS_PAM_AUTH_CANCELLED') {
+ GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
+ this.emit('auth-cancel');
+ return GLib.SOURCE_REMOVE;
+ });
+ terminate = true;
+ } else if (output === 'CS_PAM_AUTH_BUSY_TRUE') {
+ GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
+ this.emit('auth-busy', true);
+ return GLib.SOURCE_REMOVE;
+ });
+ } else if (output === 'CS_PAM_AUTH_BUSY_FALSE') {
+ GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
+ this.emit('auth-busy', false);
+ return GLib.SOURCE_REMOVE;
+ });
+ } else if (output.startsWith('CS_PAM_AUTH_SET_PROMPT_')) {
+ let match = output.match(/^CS_PAM_AUTH_SET_PROMPT_(.*)_$/);
+ if (match && match[1]) {
+ let prompt = match[1];
+ GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
+ this.emit('auth-prompt', prompt);
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+ } else if (output.startsWith('CS_PAM_AUTH_SET_ERROR_')) {
+ let match = output.match(/^CS_PAM_AUTH_SET_ERROR_(.*)_$/);
+ if (match && match[1]) {
+ let error = match[1];
+ GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
+ this.emit('auth-error', error);
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+ } else if (output.startsWith('CS_PAM_AUTH_SET_INFO_')) {
+ let match = output.match(/^CS_PAM_AUTH_SET_INFO_(.*)_$/);
+ if (match && match[1]) {
+ let info = match[1];
+ GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
+ this.emit('auth-info', info);
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+ }
+ }
+ }
+ }
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
+ global.logError('Error reading message from pam helper: ' + e.message);
+ }
+ return;
+ }
+
+ if (terminate) {
+ this._endProc();
+ return;
+ }
+
+ this._readMessages();
+ }
+}
+Signals.addSignalMethods(AuthClient.prototype);
diff --git a/js/misc/config.js.in b/js/misc/config.js.in
index 68ed93cf1b..495e5d7351 100644
--- a/js/misc/config.js.in
+++ b/js/misc/config.js.in
@@ -6,3 +6,5 @@ var PACKAGE_NAME = '@PACKAGE_NAME@';
var PACKAGE_VERSION = '@PACKAGE_VERSION@';
/* 1 if networkmanager is available, 0 otherwise */
var BUILT_NM_AGENT = @BUILT_NM_AGENT@;
+/* libexec directory */
+var LIBEXECDIR = '@LIBEXECDIR@';
diff --git a/js/misc/fileUtils.js b/js/misc/fileUtils.js
index 3374c49291..b7e03519aa 100644
--- a/js/misc/fileUtils.js
+++ b/js/misc/fileUtils.js
@@ -4,34 +4,6 @@ const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const ByteArray = imports.byteArray;
-var importNames = [
- 'mainloop',
- 'jsUnit',
- 'format',
- 'signals',
- 'lang',
- 'tweener',
- 'overrides',
- 'gettext',
- 'coverage',
- 'package',
- 'cairo',
- 'byteArray',
- 'cairoNative'
-];
-var cinnamonImportNames = [
- 'ui',
- 'misc',
- 'perf'
-];
-var giImportNames = imports.gi.GIRepository.Repository
- .get_default()
- .get_loaded_namespaces();
-var LoadedModules = [];
-var FunctionConstructor = Symbol();
-var Symbols = {};
-Symbols[FunctionConstructor] = 0..constructor.constructor;
-
function listDirAsync(file, callback) {
let allFiles = [];
file.enumerate_children_async(Gio.FILE_ATTRIBUTE_STANDARD_NAME,
@@ -111,187 +83,3 @@ function getUserDesktopDir() {
if (file.query_exists(null)) return path;
else return null;
}
-
-function findModuleIndex(path) {
- return LoadedModules.findIndex(function(cachedModule) {
- return cachedModule && cachedModule.path === path;
- });
-}
-
-function getModuleByIndex(index) {
- if (!LoadedModules[index]) {
- throw new Error('[getModuleByIndex] Module does not exist.');
- }
- return LoadedModules[index].module;
-}
-
-function unloadModule(index) {
- if (!LoadedModules[index]) {
- return;
- }
- let indexes = [];
- for (let i = 0; i < LoadedModules.length; i++) {
- if (LoadedModules[i] && LoadedModules[i].dir === LoadedModules[index].dir) {
- indexes.push(i);
- }
- }
- for (var i = 0; i < indexes.length; i++) {
- LoadedModules[indexes[i]].module = undefined;
- LoadedModules[indexes[i]].size = -1;
- }
-}
-
-function createExports({path, dir, meta, type, file, size, JS, returnIndex, reject}) {
- // Import data is stored in an array of objects and the module index is looked up by path.
- var importerData = { // changed from 'let' to 'var'.
- size,
- path,
- dir,
- module: null
- };
- // module.exports as an object holding a module's namespaces is a node convention, and is intended
- // to help interop with other libraries.
- var exports = {}; // changed from 'const' to 'var'.
- var module = { // changed from 'const' to 'var'.
- exports: exports
- };
-
- // Storing by array index that other extension classes can look up.
- let moduleIndex = findModuleIndex(path);
- if (moduleIndex > -1) {
- // Module already exists, check if its been updated
- if (size === LoadedModules[moduleIndex].size
- && LoadedModules[moduleIndex].module != null) {
- // Return the cache
- return returnIndex ? moduleIndex : LoadedModules[moduleIndex].module;
- }
- // Module has been updated
- LoadedModules[moduleIndex] = importerData;
- } else {
- LoadedModules.push(importerData);
- moduleIndex = LoadedModules.length - 1;
- }
-
- JS = `'use strict';${JS};`;
- // Regex matches the top level variable names, and appends them to the module.exports object,
- // mimicking the native CJS importer.
- const exportsRegex = /^module\.exports(\.[a-zA-Z0-9_$]+)?\s*=/m;
- const varRegex = /^(?:'use strict';){0,}(const|var|let|function|class)\s+([a-zA-Z0-9_$]+)/gm;
- var match; // changed from 'let' to 'var'.
-
- if (!exportsRegex.test(JS)) {
- while ((match = varRegex.exec(JS)) != null) {
- if (match.index === varRegex.lastIndex) {
- varRegex.lastIndex++;
- }
- // Don't modularize native imports
- if (match[2]
- && importNames.indexOf(match[2].toLowerCase()) === -1
- && giImportNames.indexOf(match[2]) === -1) {
- JS += `exports.${match[2]} = typeof ${match[2]} !== 'undefined' ? ${match[2]} : null;`;
- }
- }
- }
-
- // send_results is overridden in SearchProviderManager, so we need to make sure the send_results
- // function on the exports object, what SearchProviderManager actually has access to outside the
- // module scope, is called.
- if (type === 'search_provider') {
- JS += 'var send_results = function() {exports.send_results.apply(this, arguments)};' +
- 'var get_locale_string = function() {return exports.get_locale_string.apply(this, arguments)};';
- }
-
- // Return the exports object containing all of our top level namespaces, and include the sourceURL so
- // Spidermonkey includes the file names in stack traces.
- JS += `return module.exports;//# sourceURL=${path}`;
-
- try {
- // Create the function returning module.exports and return it to Extension so it can be called by the
- // appropriate manager.
- importerData.module = Symbols[FunctionConstructor](
- 'require',
- 'exports',
- 'module',
- '__meta',
- '__dirname',
- '__filename',
- JS
- ).call(
- exports,
- function require(path) {
- return requireModule(path, dir, meta, type);
- },
- exports,
- module,
- meta,
- dir,
- file.get_basename()
- );
-
- return returnIndex ? moduleIndex : importerData.module;
- } catch (e) {
- if (reject) {
- reject(e);
- return;
- }
- throw e;
- }
-}
-
-function requireModule(path, dir, meta, type, async = false, returnIndex = false) {
- // Allow passing through native bindings, e.g. const Cinnamon = require('gi.Cinnamon');
- // Check if this is a GI import
- if (path.substr(0, 3) === 'gi.') {
- return imports.gi[path.substr(3, path.length)];
- }
- // Check if this is a Cinnamon import
- let importPrefix = path.split('.')[0];
- if (cinnamonImportNames.indexOf(importPrefix) > -1
- && path.substr(0, importPrefix.length + 1) === `${importPrefix}.`) {
- return imports[importPrefix][path.substr(importPrefix.length + 1, path.length)];
- }
- // Check if this is a top level import
- if (importNames.indexOf(path) > -1) {
- return imports[path];
- }
- // Check the file extension
- if (path.substr(-3) !== '.js') {
- path += '.js';
- }
- // Check relative paths
- if (path[0] === '.' || path[0] !== '/') {
- path = path.replace(/\.\//g, '');
- if (dir) {
- path = dir + "/" + path;
- }
- }
- let success, JSbytes, JS;
- let file = Gio.File.new_for_commandline_arg(path);
- let fileLoadErrorMessage = '[requireModule] Unable to load file contents.';
- if (!file.query_exists(null)) {
- throw new Error("[requireModule] Path does not exist.\n" + path);
- }
-
- if (!async) {
- [success, JSbytes] = file.load_contents(null);
- if (!success) {
- throw new Error(fileLoadErrorMessage);
- }
- JS = ByteArray.toString(JSbytes);
- return createExports({path, dir, meta, type, file, size: JS.length, JS, returnIndex});
- }
- return new Promise(function(resolve, reject) {
- file.load_contents_async(null, function(object, result) {
- try {
- [success, JSbytes] = file.load_contents_finish(result);
- if (!success) {
- throw new Error(fileLoadErrorMessage);
- }
- JS = ByteArray.toString(JSbytes);
- resolve(createExports({path, dir, meta, type, file, size: JS.length, JS, returnIndex, reject}));
- } catch (e) {
- reject(e);
- }
- });
- });
-}
diff --git a/js/misc/loginManager.js b/js/misc/loginManager.js
new file mode 100644
index 0000000000..d26cdea520
--- /dev/null
+++ b/js/misc/loginManager.js
@@ -0,0 +1,309 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const Signals = imports.signals;
+
+function _log(msg) {
+ if (global.settings.get_boolean('debug-screensaver')) {
+ global.log(msg);
+ }
+}
+
+const SystemdLoginManagerIface = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+const SystemdLoginManagerProxy = Gio.DBusProxy.makeProxyWrapper(SystemdLoginManagerIface);
+
+const SystemdLoginSessionIface = `
+
+
+
+
+
+
+`;
+
+const SystemdLoginSessionProxy = Gio.DBusProxy.makeProxyWrapper(SystemdLoginSessionIface);
+
+const ConsoleKitManagerIface = `
+
+
+
+
+
+
+`;
+
+const ConsoleKitManagerProxy = Gio.DBusProxy.makeProxyWrapper(ConsoleKitManagerIface);
+
+const ConsoleKitSessionIface = `
+
+
+
+
+
+
+
+
+`;
+
+const ConsoleKitSessionProxy = Gio.DBusProxy.makeProxyWrapper(ConsoleKitSessionIface);
+
+function haveSystemd() {
+ return GLib.access("/run/systemd/seats", 0) >= 0;
+}
+
+var LoginManagerSystemd = class {
+ constructor() {
+ this._managerProxy = null;
+ this._sessionProxy = null;
+
+ this._initSession();
+ }
+
+ _initSession() {
+ _log('LoginManager: Connecting to logind...');
+
+ try {
+ this._managerProxy = new SystemdLoginManagerProxy(
+ Gio.DBus.system,
+ 'org.freedesktop.login1',
+ '/org/freedesktop/login1'
+ );
+
+ this._getCurrentSession();
+ } catch (e) {
+ global.logError('LoginManager: Failed to connect to logind: ' + e.message);
+ }
+ }
+
+ _getCurrentSession() {
+ let username = GLib.get_user_name();
+ let proc = Gio.Subprocess.new(
+ ['loginctl', 'show-user', username, '-pDisplay', '--value'],
+ Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE
+ );
+
+ proc.communicate_utf8_async(null, null, (proc, result) => {
+ try {
+ let [, stdout] = proc.communicate_utf8_finish(result);
+
+ if (!proc.get_successful()) {
+ throw new Error('loginctl command failed');
+ }
+
+ let sessionId = stdout.trim();
+ if (!sessionId) {
+ throw new Error('No session ID found');
+ }
+
+ _log(`LoginManager: Found session ID: ${sessionId}`);
+
+ this._managerProxy.GetSessionRemote(sessionId, (result, error) => {
+ if (error) {
+ global.logError('LoginManager: Failed to get session path: ' + error);
+ return;
+ }
+
+ let [sessionPath] = result;
+ _log(`LoginManager: Got session path: ${sessionPath}`);
+
+ this._connectToSession(sessionPath);
+ });
+ } catch (e) {
+ global.logError('LoginManager: Error getting logind session: ' + e.message);
+ }
+ });
+ }
+
+ _connectToSession(sessionPath) {
+ try {
+ this._sessionProxy = new SystemdLoginSessionProxy(
+ Gio.DBus.system,
+ 'org.freedesktop.login1',
+ sessionPath
+ );
+
+ _log('LoginManager: Successfully connected to logind session');
+
+ this._sessionProxy.connectSignal('Lock', () => {
+ _log('LoginManager: Received Lock signal from logind, emitting lock');
+ this.emit('lock');
+ });
+
+ this._sessionProxy.connectSignal('Unlock', () => {
+ _log('LoginManager: Received Unlock signal from logind, emitting unlock');
+ this.emit('unlock');
+ });
+
+ this._sessionProxy.connect('g-properties-changed', (proxy, changed, invalidated) => {
+ if ('Active' in changed.deep_unpack()) {
+ let active = this._sessionProxy.Active;
+ _log(`LoginManager: Session Active property changed: ${active}`);
+ if (active) {
+ _log('LoginManager: Session became active, emitting active');
+ this.emit('active');
+ }
+ }
+ });
+
+ this.emit('session-ready');
+ } catch (e) {
+ global.logError('LoginManager: Failed to connect to logind session: ' + e.message);
+ }
+ }
+
+ connectPrepareForSleep(callback) {
+ if (!this._managerProxy) {
+ return null;
+ }
+
+ return this._managerProxy.connectSignal('PrepareForSleep', (proxy, sender, [aboutToSuspend]) => {
+ _log(`LoginManager: PrepareForSleep signal received (aboutToSuspend=${aboutToSuspend})`);
+ callback(aboutToSuspend);
+ });
+ }
+
+ inhibit(reason, callback) {
+ if (!this._managerProxy) {
+ _log('LoginManager: inhibit() called but no manager proxy');
+ callback(null);
+ return;
+ }
+
+ _log(`LoginManager: Requesting sleep inhibitor: "${reason}"`);
+
+ let inVariant = GLib.Variant.new('(ssss)',
+ ['sleep', 'cinnamon-screensaver', reason, 'delay']);
+
+ this._managerProxy.call_with_unix_fd_list(
+ 'Inhibit', inVariant, 0, -1, null, null,
+ (proxy, result) => {
+ try {
+ let [outVariant_, fdList] = proxy.call_with_unix_fd_list_finish(result);
+ let fd = fdList.steal_fds()[0];
+ _log(`LoginManager: Sleep inhibitor acquired (fd=${fd})`);
+ callback(new Gio.UnixInputStream({ fd }));
+ } catch (e) {
+ global.logError('LoginManager: Error getting inhibitor: ' + e.message);
+ callback(null);
+ }
+ });
+ }
+};
+Signals.addSignalMethods(LoginManagerSystemd.prototype);
+
+var LoginManagerConsoleKit = class {
+ constructor() {
+ this._managerProxy = null;
+ this._sessionProxy = null;
+
+ this._initSession();
+ }
+
+ _initSession() {
+ _log('LoginManager: Connecting to ConsoleKit...');
+
+ try {
+ this._managerProxy = new ConsoleKitManagerProxy(
+ Gio.DBus.system,
+ 'org.freedesktop.ConsoleKit',
+ '/org/freedesktop/ConsoleKit/Manager'
+ );
+
+ this._managerProxy.GetCurrentSessionRemote((result, error) => {
+ if (error) {
+ global.logError('LoginManager: Failed to get ConsoleKit session: ' + error);
+ global.logError('LoginManager: Automatic unlocking from greeter will not work');
+ return;
+ }
+
+ let [sessionPath] = result;
+ _log(`LoginManager: Got ConsoleKit session path: ${sessionPath}`);
+
+ this._connectToSession(sessionPath);
+ });
+ } catch (e) {
+ global.logError('LoginManager: Failed to connect to ConsoleKit: ' + e.message);
+ global.logError('LoginManager: Automatic unlocking from greeter will not work');
+ }
+ }
+
+ _connectToSession(sessionPath) {
+ try {
+ this._sessionProxy = new ConsoleKitSessionProxy(
+ Gio.DBus.system,
+ 'org.freedesktop.ConsoleKit',
+ sessionPath
+ );
+
+ _log('LoginManager: Successfully connected to ConsoleKit session');
+
+ this._sessionProxy.connectSignal('Lock', () => {
+ _log('LoginManager: Received Lock signal from ConsoleKit, emitting lock');
+ this.emit('lock');
+ });
+
+ this._sessionProxy.connectSignal('Unlock', () => {
+ _log('LoginManager: Received Unlock signal from ConsoleKit, emitting unlock');
+ this.emit('unlock');
+ });
+
+ this._sessionProxy.connectSignal('ActiveChanged', (proxy, sender, [active]) => {
+ _log(`LoginManager: ConsoleKit ActiveChanged: ${active}`);
+ if (active) {
+ _log('LoginManager: Session became active, emitting active');
+ this.emit('active');
+ }
+ });
+
+ this.emit('session-ready');
+ } catch (e) {
+ global.logError('LoginManager: Failed to connect to ConsoleKit session: ' + e.message);
+ global.logError('LoginManager: Automatic unlocking from greeter will not work');
+ }
+ }
+
+ connectPrepareForSleep(callback) {
+ // ConsoleKit doesn't have PrepareForSleep
+ return null;
+ }
+
+ inhibit(reason, callback) {
+ // ConsoleKit doesn't have inhibitors
+ callback(null);
+ }
+};
+Signals.addSignalMethods(LoginManagerConsoleKit.prototype);
+
+let _loginManager = null;
+
+function getLoginManager() {
+ if (_loginManager == null) {
+ if (haveSystemd()) {
+ _loginManager = new LoginManagerSystemd();
+ } else {
+ _loginManager = new LoginManagerConsoleKit();
+ }
+ }
+
+ return _loginManager;
+}
diff --git a/js/misc/mprisPlayer.js b/js/misc/mprisPlayer.js
new file mode 100644
index 0000000000..643b2f0044
--- /dev/null
+++ b/js/misc/mprisPlayer.js
@@ -0,0 +1,627 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const Signals = imports.signals;
+
+const Interfaces = imports.misc.interfaces;
+
+const MEDIA_PLAYER_2_PATH = "/org/mpris/MediaPlayer2";
+const MEDIA_PLAYER_2_NAME = "org.mpris.MediaPlayer2";
+const MEDIA_PLAYER_2_PLAYER_NAME = "org.mpris.MediaPlayer2.Player";
+
+var PlaybackStatus = {
+ UNKNOWN: 'Unknown',
+ PLAYING: 'Playing',
+ PAUSED: 'Paused',
+ STOPPED: 'Stopped'
+};
+
+let _mprisPlayerManager = null;
+
+function getMprisPlayerManager() {
+ if (_mprisPlayerManager === null) {
+ _mprisPlayerManager = new MprisPlayerManager();
+ }
+ return _mprisPlayerManager;
+}
+
+var MprisPlayer = class MprisPlayer {
+ constructor(busName, owner) {
+ this._busName = busName;
+ this._owner = owner;
+ this._ready = false;
+ this._closed = false;
+
+ // D-Bus proxies
+ this._mediaServer = null; // org.mpris.MediaPlayer2
+ this._mediaServerPlayer = null; // org.mpris.MediaPlayer2.Player
+ this._prop = null; // org.freedesktop.DBus.Properties
+
+ this._propChangedId = 0;
+
+ this._identity = null;
+ this._desktopEntry = null;
+
+ this._playbackStatus = PlaybackStatus.UNKNOWN;
+
+ this._trackId = "";
+ this._title = "";
+ this._artist = "";
+ this._album = "";
+ this._artUrl = "";
+ this._length = 0;
+
+ this._canRaise = false;
+ this._canQuit = false;
+ this._canControl = false;
+ this._canPlay = false;
+ this._canPause = false;
+ this._canGoNext = false;
+ this._canGoPrevious = false;
+ this._canSeek = false;
+
+ this._initProxies();
+ }
+
+ _initProxies() {
+ let proxiesAcquired = 0;
+ let failCount = 0;
+ let totalProxies = 3;
+
+ let asyncReadyCb = (proxy, error, property) => {
+ if (this._closed) return;
+
+ if (error) {
+ global.logWarning(`MprisPlayer: Error acquiring ${property} for ${this._busName}: ${error}`);
+ failCount++;
+ if (proxiesAcquired + failCount === totalProxies) {
+ this.destroy();
+ }
+ return;
+ }
+
+ this[property] = proxy;
+ proxiesAcquired++;
+
+ if (proxiesAcquired === totalProxies) {
+ this._onProxiesReady();
+ }
+ };
+
+ Interfaces.getDBusProxyWithOwnerAsync(MEDIA_PLAYER_2_NAME,
+ this._busName, (p, e) => asyncReadyCb(p, e, '_mediaServer'));
+
+ Interfaces.getDBusProxyWithOwnerAsync(MEDIA_PLAYER_2_PLAYER_NAME,
+ this._busName, (p, e) => asyncReadyCb(p, e, '_mediaServerPlayer'));
+
+ Interfaces.getDBusPropertiesAsync(this._busName,
+ MEDIA_PLAYER_2_PATH, (p, e) => asyncReadyCb(p, e, '_prop'));
+ }
+
+ _onProxiesReady() {
+ if (this._closed) return;
+
+ this._ready = true;
+
+ // Get identity
+ if (this._mediaServer.Identity) {
+ this._identity = this._mediaServer.Identity;
+ } else {
+ let displayName = this._busName.replace('org.mpris.MediaPlayer2.', '');
+ this._identity = displayName.charAt(0).toUpperCase() + displayName.slice(1);
+ }
+
+ this._desktopEntry = this._mediaServer.DesktopEntry || null;
+
+ // Cache initial capabilities
+ this._updateCapabilities();
+
+ // Connect to property changes
+ this._propChangedId = this._prop.connectSignal('PropertiesChanged',
+ (proxy, sender, [iface, props]) => {
+ this._onPropertiesChanged(iface, props);
+ });
+
+ // Initial state read
+ this._updateStatus(this._mediaServerPlayer.PlaybackStatus);
+ this._updateMetadata(this._mediaServerPlayer.Metadata);
+
+ this.emit('ready');
+ }
+
+ _onPropertiesChanged(iface, props) {
+ if (this._closed) return;
+
+ let metadataChanged = false;
+ let statusChanged = false;
+ let capabilitiesChanged = false;
+
+ if (props.PlaybackStatus) {
+ this._updateStatus(props.PlaybackStatus.unpack());
+ statusChanged = true;
+ }
+
+ if (props.Metadata) {
+ this._updateMetadata(props.Metadata.deep_unpack());
+ metadataChanged = true;
+ }
+
+ if (props.CanGoNext !== undefined || props.CanGoPrevious !== undefined ||
+ props.CanPlay !== undefined || props.CanPause !== undefined ||
+ props.CanSeek !== undefined || props.CanControl !== undefined) {
+ this._updateCapabilities();
+ capabilitiesChanged = true;
+ }
+
+ if (props.Identity) {
+ this._identity = props.Identity.unpack();
+ }
+
+ if (props.DesktopEntry) {
+ this._desktopEntry = props.DesktopEntry.unpack();
+ }
+
+ if (props.CanRaise !== undefined) {
+ this._canRaise = this._mediaServer.CanRaise || false;
+ }
+
+ if (props.CanQuit !== undefined) {
+ this._canQuit = this._mediaServer.CanQuit || false;
+ }
+
+ if (metadataChanged) {
+ this.emit('metadata-changed');
+ }
+
+ if (statusChanged) {
+ this.emit('status-changed', this._playbackStatus);
+ }
+
+ if (capabilitiesChanged) {
+ this.emit('capabilities-changed');
+ }
+ }
+
+ _updateStatus(status) {
+ if (!status) {
+ this._playbackStatus = PlaybackStatus.UNKNOWN;
+ return;
+ }
+
+ switch (status) {
+ case 'Playing':
+ this._playbackStatus = PlaybackStatus.PLAYING;
+ break;
+ case 'Paused':
+ this._playbackStatus = PlaybackStatus.PAUSED;
+ break;
+ case 'Stopped':
+ this._playbackStatus = PlaybackStatus.STOPPED;
+ break;
+ default:
+ this._playbackStatus = PlaybackStatus.UNKNOWN;
+ }
+ }
+
+ _updateMetadata(metadata) {
+ if (!metadata) return;
+
+ // Track ID
+ if (metadata["mpris:trackid"]) {
+ this._trackId = metadata["mpris:trackid"].unpack();
+ } else {
+ this._trackId = "";
+ }
+
+ // Length (in microseconds)
+ if (metadata["mpris:length"]) {
+ this._length = metadata["mpris:length"].unpack();
+ } else {
+ this._length = 0;
+ }
+
+ // Artist (can be string or array)
+ if (metadata["xesam:artist"]) {
+ switch (metadata["xesam:artist"].get_type_string()) {
+ case 's':
+ this._artist = metadata["xesam:artist"].unpack();
+ break;
+ case 'as':
+ this._artist = metadata["xesam:artist"].deep_unpack().join(", ");
+ break;
+ default:
+ this._artist = "";
+ }
+ if (!this._artist) this._artist = "";
+ } else {
+ this._artist = "";
+ }
+
+ // Album
+ if (metadata["xesam:album"]) {
+ this._album = metadata["xesam:album"].unpack();
+ } else {
+ this._album = "";
+ }
+
+ // Title
+ if (metadata["xesam:title"]) {
+ this._title = metadata["xesam:title"].unpack();
+ } else {
+ this._title = "";
+ }
+
+ // Art URL
+ if (metadata["mpris:artUrl"]) {
+ this._artUrl = metadata["mpris:artUrl"].unpack();
+ } else {
+ this._artUrl = "";
+ }
+ }
+
+ _updateCapabilities() {
+ if (!this._mediaServer || !this._mediaServerPlayer) return;
+
+ this._canRaise = this._mediaServer.CanRaise || false;
+ this._canQuit = this._mediaServer.CanQuit || false;
+ this._canControl = this._mediaServerPlayer.CanControl || false;
+ this._canPlay = this._mediaServerPlayer.CanPlay || false;
+ this._canPause = this._mediaServerPlayer.CanPause || false;
+ this._canGoNext = this._mediaServerPlayer.CanGoNext || false;
+ this._canGoPrevious = this._mediaServerPlayer.CanGoPrevious || false;
+ this._canSeek = this._mediaServerPlayer.CanSeek || false;
+ }
+
+ // Identity accessors
+ getBusName() {
+ return this._busName;
+ }
+
+ getOwner() {
+ return this._owner;
+ }
+
+ getIdentity() {
+ return this._identity || "";
+ }
+
+ getDesktopEntry() {
+ return this._desktopEntry;
+ }
+
+ isReady() {
+ return this._ready;
+ }
+
+ // Capability accessors
+ canRaise() {
+ return this._canRaise;
+ }
+
+ canQuit() {
+ return this._canQuit;
+ }
+
+ canControl() {
+ return this._canControl;
+ }
+
+ canPlay() {
+ return this._canPlay;
+ }
+
+ canPause() {
+ return this._canPause;
+ }
+
+ canGoNext() {
+ return this._canGoNext;
+ }
+
+ canGoPrevious() {
+ return this._canGoPrevious;
+ }
+
+ canSeek() {
+ return this._canSeek;
+ }
+
+ // Status accessors
+ getPlaybackStatus() {
+ return this._playbackStatus;
+ }
+
+ isPlaying() {
+ return this._playbackStatus === PlaybackStatus.PLAYING;
+ }
+
+ isPaused() {
+ return this._playbackStatus === PlaybackStatus.PAUSED;
+ }
+
+ isStopped() {
+ return this._playbackStatus === PlaybackStatus.STOPPED;
+ }
+
+ // Metadata accessors
+ getTitle() {
+ return this._title;
+ }
+
+ getArtist() {
+ return this._artist;
+ }
+
+ getAlbum() {
+ return this._album;
+ }
+
+ getArtUrl() {
+ return this._artUrl;
+ }
+
+ getProcessedArtUrl() {
+ let url = this._artUrl;
+
+ // Spotify uses open.spotify.com URLs that need rewriting to i.scdn.co
+ if (this._identity && this._identity.toLowerCase() === 'spotify') {
+ url = url.replace('open.spotify.com', 'i.scdn.co');
+ }
+
+ return url;
+ }
+
+ getTrackId() {
+ return this._trackId;
+ }
+
+ getLength() {
+ return this._length;
+ }
+
+ getLengthSeconds() {
+ return this._length / 1000000;
+ }
+
+ // Playback controls
+ play() {
+ if (this._mediaServerPlayer && this._canPlay) {
+ this._mediaServerPlayer.PlayRemote();
+ }
+ }
+
+ pause() {
+ if (this._mediaServerPlayer && this._canPause) {
+ this._mediaServerPlayer.PauseRemote();
+ }
+ }
+
+ playPause() {
+ if (this._mediaServerPlayer) {
+ this._mediaServerPlayer.PlayPauseRemote();
+ }
+ }
+
+ stop() {
+ if (this._mediaServerPlayer) {
+ this._mediaServerPlayer.StopRemote();
+ }
+ }
+
+ next() {
+ if (this._mediaServerPlayer && this._canGoNext) {
+ this._mediaServerPlayer.NextRemote();
+ }
+ }
+
+ previous() {
+ if (this._mediaServerPlayer && this._canGoPrevious) {
+ this._mediaServerPlayer.PreviousRemote();
+ }
+ }
+
+ seek(offset) {
+ if (this._mediaServerPlayer && this._canSeek) {
+ this._mediaServerPlayer.SeekRemote(offset);
+ }
+ }
+
+ setPosition(trackId, position) {
+ if (this._mediaServerPlayer && this._canSeek) {
+ this._mediaServerPlayer.SetPositionRemote(trackId, position);
+ }
+ }
+
+ // Player actions
+ raise() {
+ if (this._mediaServer && this._canRaise) {
+ // Spotify workaround - it can't raise via D-Bus once closed
+ if (this._identity && this._identity.toLowerCase() === 'spotify') {
+ const Util = imports.misc.util;
+ Util.spawn(['spotify']);
+ } else {
+ this._mediaServer.RaiseRemote();
+ }
+ }
+ }
+
+ quit() {
+ if (this._mediaServer && this._canQuit) {
+ this._mediaServer.QuitRemote();
+ }
+ }
+
+ getMediaServerProxy() {
+ return this._mediaServer;
+ }
+
+ getMediaServerPlayerProxy() {
+ return this._mediaServerPlayer;
+ }
+
+ getPropertiesProxy() {
+ return this._prop;
+ }
+
+ destroy() {
+ this._closed = true;
+
+ if (this._propChangedId && this._prop) {
+ this._prop.disconnectSignal(this._propChangedId);
+ this._propChangedId = 0;
+ }
+
+ this._mediaServer = null;
+ this._mediaServerPlayer = null;
+ this._prop = null;
+
+ this.emit('closed');
+ }
+};
+Signals.addSignalMethods(MprisPlayer.prototype);
+
+/**
+ * MprisPlayerManager:
+ * Singleton that discovers and tracks all MPRIS players on the session bus.
+ *
+ * Signals:
+ * - 'player-added': (player: MprisPlayer) New player appeared
+ * - 'player-removed': (busName: string, owner: string) Player disappeared
+ */
+var MprisPlayerManager = class MprisPlayerManager {
+ constructor() {
+ this._dbus = null;
+ this._players = {}; // Keyed by owner
+ this._ownerChangedId = 0;
+
+ this._initDBus();
+ }
+
+ _initDBus() {
+ Interfaces.getDBusAsync((proxy, error) => {
+ if (error) {
+ global.logError(`MprisPlayerManager: Failed to get D-Bus proxy: ${error}`);
+ return;
+ }
+
+ this._dbus = proxy;
+
+ let nameRegex = /^org\.mpris\.MediaPlayer2\./;
+
+ this._dbus.ListNamesRemote((names) => {
+ if (!names || !names[0]) return;
+
+ for (let name of names[0]) {
+ if (nameRegex.test(name)) {
+ this._dbus.GetNameOwnerRemote(name, (owner) => {
+ if (owner && owner[0]) {
+ this._addPlayer(name, owner[0]);
+ }
+ });
+ }
+ }
+ });
+
+ this._ownerChangedId = this._dbus.connectSignal('NameOwnerChanged',
+ (proxy, sender, [name, oldOwner, newOwner]) => {
+ if (nameRegex.test(name)) {
+ if (newOwner && !oldOwner) {
+ this._addPlayer(name, newOwner);
+ } else if (oldOwner && !newOwner) {
+ this._removePlayer(name, oldOwner);
+ } else if (oldOwner && newOwner) {
+ this._changePlayerOwner(name, oldOwner, newOwner);
+ }
+ }
+ });
+ });
+ }
+
+ _addPlayer(busName, owner) {
+ if (this._players[owner]) {
+ return; // Already tracking this player
+ }
+
+ let player = new MprisPlayer(busName, owner);
+ this._players[owner] = player;
+
+ // Wait for player to be ready before emitting signal
+ player.connect('ready', () => {
+ this.emit('player-added', player);
+ });
+ }
+
+ _removePlayer(busName, owner) {
+ let player = this._players[owner];
+ if (!player) return;
+
+ delete this._players[owner];
+ player.destroy();
+
+ this.emit('player-removed', busName, owner);
+ }
+
+ _changePlayerOwner(busName, oldOwner, newOwner) {
+ this._removePlayer(busName, oldOwner);
+ this._addPlayer(busName, newOwner);
+ }
+
+ getPlayers() {
+ return Object.values(this._players);
+ }
+
+ getPlayer(owner) {
+ return this._players[owner] || null;
+ }
+
+ getPlayerByBusName(busName) {
+ for (let player of Object.values(this._players)) {
+ if (player.getBusName() === busName) {
+ return player;
+ }
+ }
+ return null;
+ }
+
+ getBestPlayer() {
+ let firstControllable = null;
+
+ for (let player of Object.values(this._players)) {
+ if (!player.isReady()) continue;
+
+ if (player.isPlaying()) {
+ return player;
+ }
+
+ if (firstControllable === null && player.canControl()) {
+ firstControllable = player;
+ }
+ }
+
+ return firstControllable;
+ }
+
+ getPlayerCount() {
+ return Object.keys(this._players).length;
+ }
+
+ hasPlayers() {
+ return this.getPlayerCount() > 0;
+ }
+
+ destroy() {
+ if (this._ownerChangedId && this._dbus) {
+ this._dbus.disconnectSignal(this._ownerChangedId);
+ this._ownerChangedId = 0;
+ }
+
+ for (let owner in this._players) {
+ this._players[owner].destroy();
+ }
+ this._players = {};
+
+ this._dbus = null;
+ }
+};
+Signals.addSignalMethods(MprisPlayerManager.prototype);
diff --git a/js/misc/powerUtils.js b/js/misc/powerUtils.js
new file mode 100644
index 0000000000..2005ef7ce6
--- /dev/null
+++ b/js/misc/powerUtils.js
@@ -0,0 +1,218 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+//
+// powerUtils.js - Shared UPower utilities for Cinnamon
+//
+// Common utility functions for working with UPower devices,
+// used by both the power applet and screensaver power widget.
+//
+
+const UPowerGlib = imports.gi.UPowerGlib;
+
+// Re-export UPower constants for convenience
+var UPDeviceKind = UPowerGlib.DeviceKind;
+var UPDeviceState = UPowerGlib.DeviceState;
+var UPDeviceLevel = UPowerGlib.DeviceLevel;
+
+/**
+ * getBatteryIconName:
+ * @percentage: battery percentage (0-100)
+ * @state: UPDeviceState value
+ *
+ * Returns the appropriate xsi-battery-level icon name for the given
+ * battery percentage and charging state.
+ */
+function getBatteryIconName(percentage, state) {
+ let charging = (state === UPDeviceState.CHARGING ||
+ state === UPDeviceState.PENDING_CHARGE);
+ let fullyCharged = (state === UPDeviceState.FULLY_CHARGED);
+
+ if (fullyCharged) {
+ return 'xsi-battery-level-100-charged-symbolic';
+ }
+
+ let levelName;
+ if (percentage < 10) {
+ levelName = 'xsi-battery-level-0';
+ } else if (percentage < 20) {
+ levelName = 'xsi-battery-level-10';
+ } else if (percentage < 30) {
+ levelName = 'xsi-battery-level-20';
+ } else if (percentage < 40) {
+ levelName = 'xsi-battery-level-30';
+ } else if (percentage < 50) {
+ levelName = 'xsi-battery-level-40';
+ } else if (percentage < 60) {
+ levelName = 'xsi-battery-level-50';
+ } else if (percentage < 70) {
+ levelName = 'xsi-battery-level-60';
+ } else if (percentage < 80) {
+ levelName = 'xsi-battery-level-70';
+ } else if (percentage < 90) {
+ levelName = 'xsi-battery-level-80';
+ } else if (percentage < 99) {
+ levelName = 'xsi-battery-level-90';
+ } else {
+ levelName = 'xsi-battery-level-100';
+ }
+
+ if (charging) {
+ levelName += '-charging';
+ }
+
+ return levelName + '-symbolic';
+}
+
+/**
+ * deviceLevelToString:
+ * @level: UPDeviceLevel value
+ *
+ * Returns a human-readable string describing the battery level.
+ */
+function deviceLevelToString(level) {
+ switch (level) {
+ case UPDeviceLevel.FULL:
+ return _("Battery full");
+ case UPDeviceLevel.HIGH:
+ return _("Battery almost full");
+ case UPDeviceLevel.NORMAL:
+ return _("Battery good");
+ case UPDeviceLevel.LOW:
+ return _("Low battery");
+ case UPDeviceLevel.CRITICAL:
+ return _("Critically low battery");
+ default:
+ return _("Unknown");
+ }
+}
+
+/**
+ * deviceKindToString:
+ * @kind: UPDeviceKind value
+ *
+ * Returns a human-readable string describing the device type.
+ */
+function deviceKindToString(kind) {
+ switch (kind) {
+ case UPDeviceKind.LINE_POWER:
+ return _("AC adapter");
+ case UPDeviceKind.BATTERY:
+ return _("Laptop battery");
+ case UPDeviceKind.UPS:
+ return _("UPS");
+ case UPDeviceKind.MONITOR:
+ return _("Monitor");
+ case UPDeviceKind.MOUSE:
+ return _("Mouse");
+ case UPDeviceKind.KEYBOARD:
+ return _("Keyboard");
+ case UPDeviceKind.PDA:
+ return _("PDA");
+ case UPDeviceKind.PHONE:
+ return _("Cell phone");
+ case UPDeviceKind.MEDIA_PLAYER:
+ return _("Media player");
+ case UPDeviceKind.TABLET:
+ return _("Tablet");
+ case UPDeviceKind.COMPUTER:
+ return _("Computer");
+ case UPDeviceKind.GAMING_INPUT:
+ return _("Gaming input");
+ case UPDeviceKind.PEN:
+ return _("Pen");
+ case UPDeviceKind.TOUCHPAD:
+ return _("Touchpad");
+ case UPDeviceKind.MODEM:
+ return _("Modem");
+ case UPDeviceKind.NETWORK:
+ return _("Network");
+ case UPDeviceKind.HEADSET:
+ return _("Headset");
+ case UPDeviceKind.SPEAKERS:
+ return _("Speakers");
+ case UPDeviceKind.HEADPHONES:
+ return _("Headphones");
+ case UPDeviceKind.VIDEO:
+ return _("Video");
+ case UPDeviceKind.OTHER_AUDIO:
+ return _("Audio device");
+ case UPDeviceKind.REMOTE_CONTROL:
+ return _("Remote control");
+ case UPDeviceKind.PRINTER:
+ return _("Printer");
+ case UPDeviceKind.SCANNER:
+ return _("Scanner");
+ case UPDeviceKind.CAMERA:
+ return _("Camera");
+ case UPDeviceKind.WEARABLE:
+ return _("Wearable");
+ case UPDeviceKind.TOY:
+ return _("Toy");
+ case UPDeviceKind.BLUETOOTH_GENERIC:
+ return _("Bluetooth device");
+ default: {
+ try {
+ return UPowerGlib.Device.kind_to_string(kind).replaceAll("-", " ").capitalize();
+ } catch {
+ return _("Unknown");
+ }
+ }
+ }
+}
+
+/**
+ * deviceKindToIcon:
+ * @kind: UPDeviceKind value
+ * @fallbackIcon: icon name to use if no specific icon for this device kind
+ *
+ * Returns an icon name appropriate for the device kind.
+ */
+function deviceKindToIcon(kind, fallbackIcon) {
+ switch (kind) {
+ case UPDeviceKind.MONITOR:
+ return "xsi-video-display";
+ case UPDeviceKind.MOUSE:
+ return "xsi-input-mouse";
+ case UPDeviceKind.KEYBOARD:
+ return "xsi-input-keyboard";
+ case UPDeviceKind.PHONE:
+ case UPDeviceKind.MEDIA_PLAYER:
+ return "xsi-phone-apple-iphone";
+ case UPDeviceKind.TABLET:
+ return "xsi-input-tablet";
+ case UPDeviceKind.COMPUTER:
+ return "xsi-computer";
+ case UPDeviceKind.GAMING_INPUT:
+ return "xsi-input-gaming";
+ case UPDeviceKind.TOUCHPAD:
+ return "xsi-input-touchpad";
+ case UPDeviceKind.HEADSET:
+ return "xsi-audio-headset";
+ case UPDeviceKind.SPEAKERS:
+ return "xsi-audio-speakers";
+ case UPDeviceKind.HEADPHONES:
+ return "xsi-audio-headphones";
+ case UPDeviceKind.PRINTER:
+ return "xsi-printer";
+ case UPDeviceKind.SCANNER:
+ return "xsi-scanner";
+ case UPDeviceKind.CAMERA:
+ return "xsi-camera-photo";
+ default:
+ if (fallbackIcon) {
+ return fallbackIcon;
+ } else {
+ return "xsi-battery-level-100";
+ }
+ }
+}
+
+/**
+ * reportsPreciseLevels:
+ * @batteryLevel: UPDeviceLevel value
+ *
+ * Returns true if the device reports precise percentage levels
+ * (battery_level == NONE indicates percentage reporting is available).
+ */
+function reportsPreciseLevels(batteryLevel) {
+ return batteryLevel == UPDeviceLevel.NONE;
+}
diff --git a/js/misc/screenSaver.js b/js/misc/screenSaver.js
index c71a3e859c..3a17d7a379 100644
--- a/js/misc/screenSaver.js
+++ b/js/misc/screenSaver.js
@@ -1,20 +1,26 @@
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
-const Lang = imports.lang;
const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const Main = imports.ui.main;
-const ScreenSaverIface =
+const ScreenSaverIface =
' \
\
\
\
\
+ \
+ \
+ \
\
\
\
\
\
\
+ \
+ \
\
\
\
@@ -23,14 +29,112 @@ const ScreenSaverIface =
const ScreenSaverInfo = Gio.DBusInterfaceInfo.new_for_xml(ScreenSaverIface);
+/**
+ * ScreenSaverService:
+ *
+ * Implements the org.cinnamon.ScreenSaver DBus interface.
+ * Routes calls to the internal screensaver (this._screenShield).
+ *
+ * Note: If internal-screensaver-enabled is false, Cinnamon must be restarted
+ * to allow the external cinnamon-screensaver daemon to claim the bus name.
+ */
+var ScreenSaverService = class ScreenSaverService {
+ constructor(screenShield) {
+ this._screenShield = screenShield;
+
+ this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(ScreenSaverIface, this);
+ this._dbusImpl.export(Gio.DBus.session, '/org/cinnamon/ScreenSaver');
+
+ Gio.DBus.session.own_name('org.cinnamon.ScreenSaver',
+ Gio.BusNameOwnerFlags.REPLACE,
+ null, null);
+
+ if (this._screenShield) {
+ this._screenShield.connect('locked', this._onLocked.bind(this));
+ this._screenShield.connect('unlocked', this._onUnlocked.bind(this));
+ }
+
+ global.log('ScreenSaverService: providing org.cinnamon.ScreenSaver interface');
+ }
+
+ _onLocked() {
+ this._emitActiveChanged(true);
+ }
+
+ _onUnlocked() {
+ this._emitActiveChanged(false);
+ }
+
+ _emitActiveChanged(isActive) {
+ if (this._dbusImpl) {
+ this._dbusImpl.emit_signal('ActiveChanged',
+ GLib.Variant.new('(b)', [isActive]));
+ }
+ }
+
+ GetActiveAsync(params, invocation) {
+ let isActive = this._screenShield.isLocked();
+ invocation.return_value(GLib.Variant.new('(b)', [isActive]));
+ }
+
+ GetActiveTimeAsync(params, invocation) {
+ let activeTime = this._screenShield.getActiveTime();
+ invocation.return_value(GLib.Variant.new('(u)', [activeTime]));
+ }
+
+ LockAsync(params, invocation) {
+ let [message] = params;
+
+ if (!Main.lockdownSettings.get_boolean('disable-lock-screen')) {
+ this._screenShield.lock(true, message || null);
+ }
+
+ invocation.return_value(null);
+ }
+
+ QuitAsync(params, invocation) {
+ // No-op for internal screensaver (can't quit Cinnamon's built-in screen shield).
+ // Exists for compatibility with legacy cinnamon-screensaver-command --exit.
+ invocation.return_value(null);
+ }
+
+ SimulateUserActivityAsync(params, invocation) {
+ this._screenShield.simulateUserActivity();
+ invocation.return_value(null);
+ }
+
+ SetActiveAsync(params, invocation) {
+ let [active] = params;
+
+ if (this._screenShield) {
+ if (active) {
+ this._screenShield.activate();
+ } else {
+ // Can't deactivate if locked
+ if (!this._screenShield.isLocked()) {
+ this._screenShield.deactivate();
+ }
+ }
+ }
+
+ invocation.return_value(null);
+ }
+};
+
+/**
+ * Legacy proxy for backward compatibility.
+ * Creates a proxy to the DBus service (which may be internal or external).
+ */
function ScreenSaverProxy() {
- var self = new Gio.DBusProxy({ g_connection: Gio.DBus.session,
- g_interface_name: ScreenSaverInfo.name,
- g_interface_info: ScreenSaverInfo,
- g_name: 'org.cinnamon.ScreenSaver',
- g_object_path: '/org/cinnamon/ScreenSaver',
- g_flags: (Gio.DBusProxyFlags.DO_NOT_AUTO_START |
- Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES) });
+ var self = new Gio.DBusProxy({
+ g_connection: Gio.DBus.session,
+ g_interface_name: ScreenSaverInfo.name,
+ g_interface_info: ScreenSaverInfo,
+ g_name: 'org.cinnamon.ScreenSaver',
+ g_object_path: '/org/cinnamon/ScreenSaver',
+ g_flags: (Gio.DBusProxyFlags.DO_NOT_AUTO_START |
+ Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES)
+ });
self.init(null);
self.screenSaverActive = false;
diff --git a/js/misc/util.js b/js/misc/util.js
index 25aee2ffcd..15efc02f41 100644
--- a/js/misc/util.js
+++ b/js/misc/util.js
@@ -769,3 +769,95 @@ function wiggle(actor, params) {
}
});
}
+
+/**
+ * switchToGreeter:
+ *
+ * Switches to the display manager's login greeter, allowing another user
+ * to log in without logging out the current user. Tries multiple display
+ * manager methods in order of preference.
+ */
+function switchToGreeter() {
+ GLib.idle_add(GLib.PRIORITY_DEFAULT, _doSwitchToGreeter);
+}
+
+function _doSwitchToGreeter() {
+ // Check if user switching is locked down
+ if (Main.lockdownSettings.get_boolean('disable-user-switching')) {
+ global.logWarning("User switching is locked down");
+ return GLib.SOURCE_REMOVE;
+ }
+
+ if (_processIsRunning('gdm')) {
+ // Old GDM
+ try {
+ spawn(['gdmflexiserver', '--startnew', 'Standard']);
+ return GLib.SOURCE_REMOVE;
+ } catch (e) {
+ global.logError('Error calling gdmflexiserver: ' + e.message);
+ }
+ }
+
+ if (_processIsRunning('gdm3')) {
+ // Newer GDM
+ try {
+ spawn(['gdmflexiserver']);
+ return GLib.SOURCE_REMOVE;
+ } catch (e) {
+ global.logError('Error calling gdmflexiserver: ' + e.message);
+ }
+ }
+
+ // Try freedesktop.org standard DBus method (works with most modern display managers)
+ let seat_path = GLib.getenv('XDG_SEAT_PATH');
+ if (seat_path) {
+ try {
+ let bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, null);
+ bus.call_sync(
+ 'org.freedesktop.DisplayManager',
+ seat_path,
+ 'org.freedesktop.DisplayManager.Seat',
+ 'SwitchToGreeter',
+ null,
+ null,
+ Gio.DBusCallFlags.NONE,
+ -1,
+ null
+ );
+ return GLib.SOURCE_REMOVE;
+ } catch (e) {
+ global.logError('Error calling SwitchToGreeter: ' + e.message);
+ }
+ }
+
+ global.logWarning('switchToGreeter: No supported display manager method available');
+ return GLib.SOURCE_REMOVE;
+}
+
+function _processIsRunning(name) {
+ try {
+ let [success, stdout] = GLib.spawn_command_line_sync('pidof ' + name);
+ return success && stdout.length > 0;
+ } catch (e) {
+ return false;
+ }
+}
+
+/**
+ * getTtyVals:
+ *
+ * Determines the VT number of the current graphical session and a free
+ * text console VT. Used by the backup locker to tell the user which
+ * Ctrl+Alt+F key to use for recovery.
+ *
+ * Returns: (array): [termTty, sessionTty] as integers
+ */
+function getTtyVals() {
+ let sessionTty = parseInt(GLib.getenv('XDG_VTNR'));
+ if (isNaN(sessionTty))
+ sessionTty = 7;
+
+ let termTty = sessionTty !== 2 ? 2 : 1;
+
+ return [termTty, sessionTty];
+}
diff --git a/js/ui/appSwitcher/appSwitcher.js b/js/ui/appSwitcher/appSwitcher.js
index 19ed51d2f5..49f80c71b2 100644
--- a/js/ui/appSwitcher/appSwitcher.js
+++ b/js/ui/appSwitcher/appSwitcher.js
@@ -132,10 +132,10 @@ AppSwitcher.prototype = {
},
_setupModal: function () {
- this._haveModal = Main.pushModal(this.actor);
+ this._haveModal = Main.pushModal(this.actor, undefined, undefined, Cinnamon.ActionMode.SYSTEM_MODAL);
if (!this._haveModal) {
// Probably someone else has a pointer grab, try again with keyboard only
- this._haveModal = Main.pushModal(this.actor, global.get_current_time(), Meta.ModalOptions.POINTER_ALREADY_GRABBED);
+ this._haveModal = Main.pushModal(this.actor, global.get_current_time(), Meta.ModalOptions.POINTER_ALREADY_GRABBED, Cinnamon.ActionMode.SYSTEM_MODAL);
}
if (!this._haveModal)
this._failedGrabAction();
diff --git a/js/ui/appletManager.js b/js/ui/appletManager.js
index b8bfc679d1..b7338f4fa2 100644
--- a/js/ui/appletManager.js
+++ b/js/ui/appletManager.js
@@ -10,7 +10,6 @@ const Applet = imports.ui.applet;
const Extension = imports.ui.extension;
const ModalDialog = imports.ui.modalDialog;
const Dialog = imports.ui.dialog;
-const {getModuleByIndex} = imports.misc.fileUtils;
const {queryCollection} = imports.misc.util;
const Gettext = imports.gettext;
const Panel = imports.ui.panel;
@@ -592,14 +591,13 @@ function createApplet(extension, appletDefinition, panel = null) {
let applet;
try {
- let module = getModuleByIndex(extension.moduleIndex);
- if (!module) {
+ if (!extension.module) {
return null;
}
// FIXME: Panel height is now available before an applet is initialized,
// so we don't need to pass it to the constructor anymore, but would
// require a compatibility clean-up effort.
- applet = module.main(extension.meta, orientation, panel.height, applet_id);
+ applet = extension.module.main(extension.meta, orientation, panel.height, applet_id);
} catch (e) {
Extension.logError(`Failed to evaluate 'main' function on applet: ${uuid}/${applet_id}`, uuid, e);
return null;
diff --git a/js/ui/backgroundManager.js b/js/ui/backgroundManager.js
index a843c4bcd0..fc5deee1e6 100644
--- a/js/ui/backgroundManager.js
+++ b/js/ui/backgroundManager.js
@@ -1,7 +1,6 @@
// -*- mode: js2; indent-tabs-mode: nil; js2-basic-offset: 4 -*-
const Gio = imports.gi.Gio;
-const Lang = imports.lang;
const Meta = imports.gi.Meta;
const LOGGING = false;
@@ -15,23 +14,23 @@ var BackgroundManager = class {
this._gnomeSettings = new Gio.Settings({ schema_id: "org.gnome.desktop.background" });
this._cinnamonSettings = new Gio.Settings({ schema_id: "org.cinnamon.desktop.background" });
- this.color_shading_type = this._gnomeSettings.get_string("color-shading-type");
- this._gnomeSettings.connect("changed::color-shading-type", Lang.bind(this, this._onColorShadingTypeChanged));
+ this.colorShadingType = this._gnomeSettings.get_string("color-shading-type");
+ this._gnomeSettings.connect("changed::color-shading-type", this._onColorShadingTypeChanged.bind(this));
- this.picture_options = this._gnomeSettings.get_string("picture-options");
- this._gnomeSettings.connect("changed::picture-options", Lang.bind(this, this._onPictureOptionsChanged));
+ this.pictureOptions = this._gnomeSettings.get_string("picture-options");
+ this._gnomeSettings.connect("changed::picture-options", this._onPictureOptionsChanged.bind(this));
- this.picture_uri = this._gnomeSettings.get_string("picture-uri");
- this._gnomeSettings.connect("changed::picture-uri", Lang.bind(this, this._onPictureURIChanged));
+ this.pictureUri = this._gnomeSettings.get_string("picture-uri");
+ this._gnomeSettings.connect("changed::picture-uri", this._onPictureURIChanged.bind(this));
- this.primary_color = this._gnomeSettings.get_string("primary-color");
- this._gnomeSettings.connect("changed::primary-color", Lang.bind(this, this._onPrimaryColorChanged));
+ this.primaryColor = this._gnomeSettings.get_string("primary-color");
+ this._gnomeSettings.connect("changed::primary-color", this._onPrimaryColorChanged.bind(this));
- this.secondary_color = this._gnomeSettings.get_string("secondary-color");
- this._gnomeSettings.connect("changed::secondary-color", Lang.bind(this, this._onSecondaryColorChanged));
+ this.secondaryColor = this._gnomeSettings.get_string("secondary-color");
+ this._gnomeSettings.connect("changed::secondary-color", this._onSecondaryColorChanged.bind(this));
- this.picture_opacity = this._gnomeSettings.get_int("picture-opacity");
- this._gnomeSettings.connect("changed::picture-opacity", Lang.bind(this, this._onPictureOpacityChanged));
+ this.pictureOpacity = this._gnomeSettings.get_int("picture-opacity");
+ this._gnomeSettings.connect("changed::picture-opacity", this._onPictureOpacityChanged.bind(this));
}
showBackground() {
@@ -53,7 +52,7 @@ var BackgroundManager = class {
}
_onColorShadingTypeChanged(schema, key) {
- let oldValue = this.color_shading_type
+ let oldValue = this.colorShadingType
let newValue = this._gnomeSettings.get_string(key);
if (oldValue != newValue) {
let cinnamonValue = this._cinnamonSettings.get_string(key);
@@ -61,12 +60,12 @@ var BackgroundManager = class {
if (LOGGING) global.log("BackgroundManager: %s changed (%s --> %s)".format(key, oldValue, newValue));
this._cinnamonSettings.set_string(key, newValue);
}
- this.color_shading_type = newValue;
+ this.colorShadingType = newValue;
}
}
_onPictureOptionsChanged(schema, key) {
- let oldValue = this.picture_options
+ let oldValue = this.pictureOptions
let newValue = this._gnomeSettings.get_string(key);
if (oldValue != newValue) {
let cinnamonValue = this._cinnamonSettings.get_string(key);
@@ -74,12 +73,12 @@ var BackgroundManager = class {
if (LOGGING) global.log("BackgroundManager: %s changed (%s --> %s)".format(key, oldValue, newValue));
this._cinnamonSettings.set_string(key, newValue);
}
- this.picture_options = newValue;
+ this.pictureOptions = newValue;
}
}
_onPictureURIChanged(schema, key) {
- let oldValue = this.picture_uri
+ let oldValue = this.pictureUri
let newValue = this._gnomeSettings.get_string(key);
if (oldValue != newValue) {
let cinnamonValue = this._cinnamonSettings.get_string(key);
@@ -87,12 +86,12 @@ var BackgroundManager = class {
if (LOGGING) global.log("BackgroundManager: %s changed (%s --> %s)".format(key, oldValue, newValue));
this._cinnamonSettings.set_string(key, newValue);
}
- this.picture_uri = newValue;
+ this.pictureUri = newValue;
}
}
_onPrimaryColorChanged(schema, key) {
- let oldValue = this.primary_color
+ let oldValue = this.primaryColor
let newValue = this._gnomeSettings.get_string(key);
if (oldValue != newValue) {
let cinnamonValue = this._cinnamonSettings.get_string(key);
@@ -100,12 +99,12 @@ var BackgroundManager = class {
if (LOGGING) global.log("BackgroundManager: %s changed (%s --> %s)".format(key, oldValue, newValue));
this._cinnamonSettings.set_string(key, newValue);
}
- this.primary_color = newValue;
+ this.primaryColor = newValue;
}
}
_onSecondaryColorChanged(schema, key) {
- let oldValue = this.secondary_color
+ let oldValue = this.secondaryColor
let newValue = this._gnomeSettings.get_string(key);
if (oldValue != newValue) {
let cinnamonValue = this._cinnamonSettings.get_string(key);
@@ -113,12 +112,12 @@ var BackgroundManager = class {
if (LOGGING) global.log("BackgroundManager: %s changed (%s --> %s)".format(key, oldValue, newValue));
this._cinnamonSettings.set_string(key, newValue);
}
- this.secondary_color = newValue;
+ this.secondaryColor = newValue;
}
}
_onPictureOpacityChanged(schema, key) {
- let oldValue = this.picture_opacity
+ let oldValue = this.pictureOpacity
let newValue = this._gnomeSettings.get_int(key);
if (oldValue != newValue) {
let cinnamonValue = this._cinnamonSettings.get_int(key);
@@ -126,7 +125,7 @@ var BackgroundManager = class {
if (LOGGING) global.log("BackgroundManager: %s changed (%s --> %s)".format(key, oldValue, newValue));
this._cinnamonSettings.set_int(key, newValue);
}
- this.picture_opacity = newValue;
+ this.pictureOpacity = newValue;
}
}
};
diff --git a/js/ui/checkBox.js b/js/ui/checkBox.js
index 8f556cc995..769c78494e 100644
--- a/js/ui/checkBox.js
+++ b/js/ui/checkBox.js
@@ -1,164 +1,14 @@
const Clutter = imports.gi.Clutter;
const GObject = imports.gi.GObject;
const Pango = imports.gi.Pango;
-const Cinnamon = imports.gi.Cinnamon;
const St = imports.gi.St;
-const Params = imports.misc.params;
-const Lang = imports.lang;
-
-var CheckBoxContainer = class {
- constructor() {
- this.actor = new Cinnamon.GenericContainer({ y_align: St.Align.MIDDLE });
- this.actor.connect('get-preferred-width',
- Lang.bind(this, this._getPreferredWidth));
- this.actor.connect('get-preferred-height',
- Lang.bind(this, this._getPreferredHeight));
- this.actor.connect('allocate',
- Lang.bind(this, this._allocate));
- this.actor.connect('style-changed', Lang.bind(this,
- function() {
- let node = this.actor.get_theme_node();
- this._spacing = Math.round(node.get_length('spacing'));
- }));
- this.actor.request_mode = Clutter.RequestMode.HEIGHT_FOR_WIDTH;
-
- this._box = new St.Bin();
- this.actor.add_actor(this._box);
-
- this.label = new St.Label();
- this.label.clutter_text.set_line_wrap(false);
- this.label.clutter_text.set_ellipsize(Pango.EllipsizeMode.NONE);
- this.actor.add_actor(this.label);
-
- this._spacing = 0;
- }
-
- _getPreferredWidth(actor, forHeight, alloc) {
- let node = this.actor.get_theme_node();
- forHeight = node.adjust_for_height(forHeight);
-
- let [minBoxWidth, natBoxWidth] = this._box.get_preferred_width(forHeight);
- let boxNode = this._box.get_theme_node();
- [minBoxWidth, natBoxWidth] = boxNode.adjust_preferred_width(minBoxWidth, natBoxWidth);
-
- let [minLabelWidth, natLabelWidth] = this.label.get_preferred_width(forHeight);
- let labelNode = this.label.get_theme_node();
- [minLabelWidth, natLabelWidth] = labelNode.adjust_preferred_width(minLabelWidth, natLabelWidth);
-
- let min = minBoxWidth + minLabelWidth + this._spacing;
- let nat = natBoxWidth + natLabelWidth + this._spacing;
- [min, nat] = node.adjust_preferred_width(min, nat);
-
- alloc.min_size = min;
- alloc.natural_size = nat;
- }
-
- _getPreferredHeight(actor, forWidth, alloc) {
- let [minBoxHeight, natBoxHeight] =
- this._box.get_preferred_height(-1);
- let [minLabelHeight, natLabelHeight] =
- this.label.get_preferred_height(-1);
-
- alloc.min_size = Math.max(minBoxHeight, minLabelHeight);
- alloc.natural_size = Math.max(natBoxHeight, natLabelHeight);
- }
-
- _allocate(actor, box, flags) {
- let availWidth = box.x2 - box.x1;
- let availHeight = box.y2 - box.y1;
-
- let childBox = new Clutter.ActorBox();
- let [minBoxWidth, natBoxWidth] =
- this._box.get_preferred_width(-1);
- let [minBoxHeight, natBoxHeight] =
- this._box.get_preferred_height(-1);
- childBox.x1 = box.x1;
- childBox.x2 = box.x1 + natBoxWidth;
- if (availHeight > natBoxHeight) childBox.y1 = box.y1 + (availHeight-natBoxHeight)/2;
- else childBox.y1 = box.y1;
- childBox.y2 = childBox.y1 + natBoxHeight;
- this._box.allocate(childBox, flags);
-
- let [minLabelWidth, natLabelWidth] =
- this.label.get_preferred_width(-1);
- let [minLabelHeight, natLabelHeight] =
- this.label.get_preferred_height(-1);
- childBox.x1 = box.x1 + natBoxWidth + this._spacing;
- childBox.x2 = childBox.x1 + availWidth - natBoxWidth - this._spacing;
- if (availHeight > natLabelHeight) childBox.y1 = box.y1 + (availHeight-natLabelHeight)/2;
- else childBox.y1 = box.y1;
- childBox.y2 = childBox.y1 + natLabelHeight;
- this.label.allocate(childBox, flags);
- }
-}
-
-var CheckBoxBase = class {
- constructor(checkedState, params) {
- this._params = { style_class: 'check-box',
- button_mask: St.ButtonMask.ONE,
- toggle_mode: true,
- can_focus: true,
- x_fill: true,
- y_fill: true,
- y_align: St.Align.MIDDLE };
-
- if (params != undefined) {
- this._params = Params.parse(params, this._params);
- }
-
- this.actor = new St.Button(this._params);
- this.actor._delegate = this;
- this.actor.checked = checkedState;
- }
-
- setToggleState(checkedState) {
- this.actor.checked = checkedState;
- }
-
- toggle() {
- this.setToggleState(!this.actor.checked);
- }
-
- destroy() {
- this.actor.destroy();
- }
-}
-
-var CheckButton = class extends CheckBoxBase {
- constructor(checkedState, params) {
- super(checkedState, params);
- this.checkmark = new St.Bin();
- this.actor.set_child(this.checkmark);
- }
-}
-
-var CheckBox = class extends CheckBoxBase {
- constructor(label, params, checkedState) {
- super(checkedState, params);
-
- this._container = new CheckBoxContainer();
- this.actor.set_child(this._container.actor);
-
- if (label)
- this.setLabel(label);
- }
-
- setLabel(label) {
- this._container.label.set_text(label);
- }
-
- getLabelActor() {
- return this._container.label;
- }
-}
-
-var CheckBox2 = GObject.registerClass(
-class CheckBox2 extends St.Button {
+var CheckBox = GObject.registerClass(
+class CheckBox extends St.Button {
_init(label) {
let container = new St.BoxLayout();
super._init({
- style_class: 'check-box-2',
+ style_class: 'check-box',
important: true,
child: container,
button_mask: St.ButtonMask.ONE,
diff --git a/js/ui/cinnamonEntry.js b/js/ui/cinnamonEntry.js
index 13da2319bf..b1a59939ff 100644
--- a/js/ui/cinnamonEntry.js
+++ b/js/ui/cinnamonEntry.js
@@ -1,8 +1,6 @@
const Clutter = imports.gi.Clutter;
const Cinnamon = imports.gi.Cinnamon;
const GObject = imports.gi.GObject;
-const Gtk = imports.gi.Gtk;
-const Lang = imports.lang;
const Pango = imports.gi.Pango;
const St = imports.gi.St;
@@ -10,18 +8,11 @@ const Main = imports.ui.main;
const Params = imports.misc.params;
const PopupMenu = imports.ui.popupMenu;
+var _EntryMenu = class extends PopupMenu.PopupMenu {
+ constructor (entry, params) {
+ super(entry, 0, St.Side.TOP)
-function _EntryMenu(entry, params) {
- this._init(entry, params);
-};
-
-_EntryMenu.prototype = {
- __proto__: PopupMenu.PopupMenu.prototype,
-
- _init: function(entry, params) {
- params = Params.parse (params, { isPassword: false });
-
- PopupMenu.PopupMenu.prototype._init.call(this, entry, St.Side.TOP);
+ params = Params.parse(params, { isPassword: false });
this.actor.add_style_class_name('entry-context-menu');
@@ -31,29 +22,28 @@ _EntryMenu.prototype = {
// Populate menu
let item;
item = new PopupMenu.PopupMenuItem(_("Copy"));
- item.connect('activate', Lang.bind(this, this._onCopyActivated));
+ item.connect('activate', this._onCopyActivated.bind(this));
this.addMenuItem(item);
this._copyItem = item;
item = new PopupMenu.PopupMenuItem(_("Paste"));
- item.connect('activate', Lang.bind(this, this._onPasteActivated));
+ item.connect('activate', this._onPasteActivated.bind(this));
this.addMenuItem(item);
this._pasteItem = item;
this._passwordItem = null;
if (params.isPassword) {
item = new PopupMenu.PopupMenuItem('');
- item.connect('activate', Lang.bind(this,
- this._onPasswordActivated));
+ item.connect('activate', this._onPasswordActivated.bind(this));
this.addMenuItem(item);
this._passwordItem = item;
}
Main.uiGroup.add_actor(this.actor);
this.actor.hide();
- },
+ }
- open: function() {
+ open() {
this._updatePasteItem();
this._updateCopyItem();
if (this._passwordItem)
@@ -67,46 +57,44 @@ _EntryMenu.prototype = {
this.shiftToPosition(x);
}
- PopupMenu.PopupMenu.prototype.open.call(this);
- },
+ super.open();
+ }
- _updateCopyItem: function() {
+ _updateCopyItem() {
let selection = this._entry.clutter_text.get_selection();
this._copyItem.setSensitive(selection && selection != '');
- },
+ }
- _updatePasteItem: function() {
- this._clipboard.get_text(St.ClipboardType.CLIPBOARD, Lang.bind(this,
- function(clipboard, text) {
- this._pasteItem.setSensitive(text && text != '');
- }));
- },
+ _updatePasteItem() {
+ this._clipboard.get_text(St.ClipboardType.CLIPBOARD, (clipboard, text) => {
+ this._pasteItem.setSensitive(text && text != '');
+ });
+ }
- _updatePasswordItem: function() {
+ _updatePasswordItem() {
let textHidden = (this._entry.clutter_text.password_char);
if (textHidden)
this._passwordItem.label.set_text(_("Show Text"));
else
this._passwordItem.label.set_text(_("Hide Text"));
- },
+ }
- _onCopyActivated: function() {
+ _onCopyActivated() {
let selection = this._entry.clutter_text.get_selection();
this._clipboard.set_text(St.ClipboardType.CLIPBOARD, selection);
- },
-
- _onPasteActivated: function() {
- this._clipboard.get_text(St.ClipboardType.CLIPBOARD, Lang.bind(this,
- function(clipboard, text) {
- if (!text)
- return;
- this._entry.clutter_text.delete_selection();
- let pos = this._entry.clutter_text.get_cursor_position();
- this._entry.clutter_text.insert_text(text, pos);
- }));
- },
-
- _onPasswordActivated: function() {
+ }
+
+ _onPasteActivated() {
+ this._clipboard.get_text(St.ClipboardType.CLIPBOARD, (clipboard, text) => {
+ if (!text)
+ return;
+ this._entry.clutter_text.delete_selection();
+ let pos = this._entry.clutter_text.get_cursor_position();
+ this._entry.clutter_text.insert_text(text, pos);
+ });
+ }
+
+ _onPasswordActivated() {
let visible = !!(this._entry.clutter_text.password_char);
this._entry.clutter_text.set_password_char(visible ? '' : '\u25cf');
}
diff --git a/js/ui/deskletManager.js b/js/ui/deskletManager.js
index f3763355c3..8676b9edea 100644
--- a/js/ui/deskletManager.js
+++ b/js/ui/deskletManager.js
@@ -4,6 +4,7 @@ const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const St = imports.gi.St;
const Meta = imports.gi.Meta;
+const Cinnamon = imports.gi.Cinnamon;
const Mainloop = imports.mainloop;
const Lang = imports.lang;
@@ -11,7 +12,6 @@ const Desklet = imports.ui.desklet;
const DND = imports.ui.dnd;
const Extension = imports.ui.extension;
const Main = imports.ui.main;
-const {getModuleByIndex} = imports.misc.fileUtils;
const {queryCollection} = imports.misc.util;
// Maps uuid -> importer object (desklet directory tree)
@@ -332,7 +332,7 @@ function _createDesklets(extension, deskletDefinition) {
let desklet;
try {
- desklet = getModuleByIndex(extension.moduleIndex).main(extension.meta, desklet_id);
+ desklet = extension.module.main(extension.meta, desklet_id);
} catch (e) {
Extension.logError('Failed to evaluate \'main\' function on desklet: ' + uuid + "/" + desklet_id, e);
return null;
@@ -611,7 +611,7 @@ DeskletContainer.prototype = {
global.stage.connect('leave-event', Lang.bind(this, this.handleStageEvent))
];
- if (Main.pushModal(this.actor)) {
+ if (Main.pushModal(this.actor, undefined, undefined, Cinnamon.ActionMode.POPUP)) {
this.isModal = true;
}
},
diff --git a/js/ui/dnd.js b/js/ui/dnd.js
index 50072c6fbc..53ff25bf18 100644
--- a/js/ui/dnd.js
+++ b/js/ui/dnd.js
@@ -172,7 +172,7 @@ var _Draggable = new Lang.Class({
_grabEvents: function(event) {
if (!this._eventsGrabbed) {
- this._eventsGrabbed = Main.pushModal(_getEventHandlerActor());
+ this._eventsGrabbed = Main.pushModal(_getEventHandlerActor(), undefined, undefined, Cinnamon.ActionMode.NORMAL);
if (this._eventsGrabbed) {
this.drag_device = event.get_device()
this.drag_device.grab(_getEventHandlerActor());
diff --git a/js/ui/edgeFlip.js b/js/ui/edgeFlip.js
deleted file mode 100644
index 986e884927..0000000000
--- a/js/ui/edgeFlip.js
+++ /dev/null
@@ -1,73 +0,0 @@
-// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
-const Main = imports.ui.main;
-const Clutter = imports.gi.Clutter;
-const St = imports.gi.St;
-const Mainloop = imports.mainloop;
-const Lang = imports.lang;
-const Cinnamon = imports.gi.Cinnamon;
-
-var EdgeFlipper = class {
- constructor(side, func) {
- this.side = side;
- this.func = func;
-
- this.enabled = true;
- this.delay = 1000;
- this.entered = false;
- this.activated = false;
-
- this._checkOver();
- }
-
- _checkOver() {
- if (this.enabled) {
- let mask;
- [this.xMouse, this.yMouse, mask] = global.get_pointer();
- if (!(mask & Clutter.ModifierType.BUTTON1_MASK)) {
- if (this.side == St.Side.RIGHT){
- if (this.xMouse + 2 > global.screen_width){
- this._onMouseEnter();
- } else {
- this._onMouseLeave();
- }
- } else if (this.side == St.Side.LEFT){
- if (this.xMouse < 2 ){
- this._onMouseEnter();
- } else {
- this._onMouseLeave();
- }
- } else if (this.side == St.Side.BOTTOM){
- if (this.yMouse + 2 > global.screen_height) {
- this._onMouseEnter();
- } else {
- this._onMouseLeave();
- }
- } else if (this.side == St.Side.TOP){
- if (this.yMouse < 2){
- this._onMouseEnter();
- } else {
- this._onMouseLeave();
- }
- }
- }
- Mainloop.timeout_add(Math.max(this.delay, 200), Lang.bind(this, this._checkOver));
- }
- }
-
- _onMouseEnter() {
- this.entered = true;
- Mainloop.timeout_add(this.delay, Lang.bind(this, this._check));
- }
-
- _check() {
- if (this.entered && this.enabled && !this.activated){
- this.func();
- this.activated = true;
- }
- }
-
- _onMouseLeave() {
- this.entered = false;
- this.activated = false;
- }
-};
diff --git a/js/ui/endSessionDialog.js b/js/ui/endSessionDialog.js
index 095011740f..2871c84eee 100644
--- a/js/ui/endSessionDialog.js
+++ b/js/ui/endSessionDialog.js
@@ -155,20 +155,14 @@ class EndSessionDialog extends ModalDialog.ModalDialog {
if (canSuspend) {
this.addButton({
label: _("Suspend"),
- action: () => {
- this._dialogProxy.SuspendRemote();
- this.close();
- },
+ action: this._dialogProxy.SuspendRemote.bind(this._dialogProxy),
});
}
if (canHibernate) {
this.addButton({
label: _("Hibernate"),
- action: () => {
- this._dialogProxy.HibernateRemote();
- this.close();
- }
+ action: this._dialogProxy.HibernateRemote.bind(this._dialogProxy),
});
}
@@ -278,6 +272,7 @@ class EndSessionDialog extends ModalDialog.ModalDialog {
_presentInhibitorInfo(inhibitorInfos) {
this._removeDelayTimer();
this.clearButtons();
+ this._applicationsSection.list.destroy_all_children();
this._messageDialogContent.description = null;
const infos = inhibitorInfos;
diff --git a/js/ui/expo.js b/js/ui/expo.js
index 526ce3b1c3..36b453e6f8 100644
--- a/js/ui/expo.js
+++ b/js/ui/expo.js
@@ -267,7 +267,7 @@ Expo.prototype = {
return;
this.beforeShow();
// Do this manually instead of using _syncInputMode, to handle failure
- if (!Main.pushModal(this._group))
+ if (!Main.pushModal(this._group, undefined, undefined, Cinnamon.ActionMode.OVERVIEW))
return;
this._modal = true;
this._animateVisible();
@@ -389,7 +389,7 @@ Expo.prototype = {
if (this._shown) {
if (!this._modal) {
- if (Main.pushModal(this._group))
+ if (Main.pushModal(this._group, undefined, undefined, Cinnamon.ActionMode.OVERVIEW))
this._modal = true;
else
this.hide();
diff --git a/js/ui/extension.js b/js/ui/extension.js
index 08197a7492..4a8b8b6493 100644
--- a/js/ui/extension.js
+++ b/js/ui/extension.js
@@ -15,7 +15,6 @@ const DeskletManager = imports.ui.deskletManager;
const ExtensionSystem = imports.ui.extensionSystem;
const SearchProviderManager = imports.ui.searchProviderManager;
const Main = imports.ui.main;
-const {requireModule, unloadModule, getModuleByIndex} = imports.misc.fileUtils;
const {queryCollection} = imports.misc.util;
var State = {
@@ -26,17 +25,6 @@ var State = {
X11_ONLY: 4
};
-// Xlets using imports.gi.NMClient. This should be removed in Cinnamon 4.2+,
-// after these applets have been updated on Spices.
-var knownCinnamon4Conflicts = [
- // Applets
- 'turbonote@iksws.com.b',
- 'vnstat@linuxmint.com',
- 'netusagemonitor@pdcurtis',
- // Desklets
- 'netusage@30yavash.com'
-];
-
var x11Only = [
"systray@cinnamon.org"
]
@@ -96,6 +84,271 @@ function _createExtensionType(name, folder, manager, overrides){
*/
var startTime;
var extensions = [];
+
+// Track the currently loading extension for require() calls during module initialization
+var currentlyLoadingExtension = null;
+// UUIDs that have already been warned about using deprecated require()
+var requireWarned = new Set();
+
+// Stack-based module.exports compatibility for Node.js-style modules
+var moduleStack = [];
+// Cache of module.exports overrides, keyed by module path
+var moduleExportsCache = {};
+
+Object.defineProperty(globalThis, 'module', {
+ get: function() {
+ if (moduleStack.length > 0) {
+ return moduleStack[moduleStack.length - 1];
+ }
+ return undefined;
+ },
+ configurable: true
+});
+
+// Also provide 'exports' as a shorthand (some code uses it directly)
+Object.defineProperty(globalThis, 'exports', {
+ get: function() {
+ if (moduleStack.length > 0) {
+ return moduleStack[moduleStack.length - 1].exports;
+ }
+ return undefined;
+ },
+ configurable: true
+});
+
+/**
+ * getXletFromStack:
+ *
+ * Get the calling xlet by examining the stack trace.
+ *
+ * Returns: The Extension object if found, null otherwise
+ */
+function getXletFromStack() {
+ let stack = new Error().stack.split('\n');
+ for (let i = 1; i < stack.length; i++) {
+ for (let folder of ['applets', 'desklets', 'extensions', 'search_providers']) {
+ let match = stack[i].match(new RegExp(`/${folder}/([^/]+)/`));
+ if (match) {
+ return getExtension(match[1]) || getExtension(match[1].replace('!', ''));
+ }
+ }
+ }
+ return null;
+}
+
+/**
+ * getCurrentExtension:
+ *
+ * Get the current xlet's Extension object. Can be called during module
+ * initialization or at runtime.
+ *
+ * Usage in xlets:
+ * const Extension = imports.ui.extension;
+ * const Me = Extension.getCurrentExtension();
+ * const MyModule = Me.imports.myModule;
+ *
+ * Returns: The Extension object for the calling xlet
+ */
+function getCurrentExtension() {
+ return currentlyLoadingExtension || getXletFromStack();
+}
+
+/**
+ * xletRequire:
+ * @path (string): The module path to require
+ *
+ * ********************* DEPRECATED ************************
+ * *** Use getCurrentExtension() to import local modules ***
+ * *********************************************************
+ *
+ * Global require function for xlets. Supports:
+ * - Relative paths: './calendar' -> extension.imports.calendar
+ * - GI imports: 'gi.St' -> imports.gi.St
+ * - Cinnamon imports: 'ui.main' -> imports.ui.main
+ *
+ * Returns: The required module
+ */
+var _FunctionConstructor = (0).constructor.constructor;
+
+function _evalModule(extension, resolvedPath) {
+ let filePath = `${extension.meta.path}/${resolvedPath}.js`;
+ let file = Gio.File.new_for_path(filePath);
+ let [success, contents] = file.load_contents(null);
+ if (!success) {
+ throw new Error(`Failed to load ${filePath}`);
+ }
+
+ let source = ByteArray.toString(contents);
+ let exports = {};
+ let module = { exports: exports };
+
+ // Regex matches top level declarations and appends them to exports,
+ // mimicking how the native CJS importer handles var/function.
+ let re = /^(?:const|var|let|function|class)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/gm;
+ let match;
+ let exportLines = '';
+ while ((match = re.exec(source)) !== null) {
+ exportLines += `if(typeof ${match[1]}!=='undefined')exports.${match[1]}=${match[1]};`;
+ }
+
+ source = `'use strict';${source};${exportLines}return module.exports;//# sourceURL=${filePath}`;
+
+ _FunctionConstructor(
+ 'require', 'exports', 'module', source
+ ).call(
+ exports,
+ function require(path) { return xletRequire(path); },
+ exports,
+ module
+ );
+
+ return module.exports;
+}
+
+function _requireLocal(extension, path) {
+ let parts = path.replace(/^\.\//, '').replace(/\.js$/, '').split('/');
+ let resolvedPath = parts.filter(p => p !== '..').join('/');
+ let cacheKey = `${extension.meta.path}/${resolvedPath}`;
+
+ if (cacheKey in moduleExportsCache) {
+ return moduleExportsCache[cacheKey];
+ }
+
+ let moduleObj = { exports: {} };
+ moduleObj._originalExports = moduleObj.exports;
+ moduleStack.push(moduleObj);
+
+ let nativeModule;
+ try {
+ nativeModule = extension.imports;
+ for (let part of parts) {
+ if (part === '..') continue;
+ nativeModule = nativeModule[part];
+ }
+ } finally {
+ moduleStack.pop();
+ }
+
+ let result;
+
+ let exportsReplaced = moduleObj.exports !== moduleObj._originalExports;
+ let exportsMutated = Object.getOwnPropertyNames(moduleObj._originalExports).length > 0;
+
+ if (exportsReplaced) {
+ result = moduleObj.exports;
+ } else if (exportsMutated) {
+ result = moduleObj._originalExports;
+ } else {
+ // CJS only exports var/function declarations as module properties.
+ // let/const/class values are inaccessible via the native module object.
+ // Fall back to evaluating the source with module/exports in scope.
+ result = _evalModule(extension, resolvedPath);
+ }
+
+ moduleExportsCache[cacheKey] = result;
+ return result;
+}
+
+function xletRequire(path) {
+ let extension = currentlyLoadingExtension || getXletFromStack();
+ if (!extension) {
+ throw new Error(`require() called outside of xlet context: ${path}`);
+ }
+
+ if (!requireWarned.has(extension.uuid)) {
+ requireWarned.add(extension.uuid);
+ global.logWarning(`${extension.uuid}: require() and module.exports are deprecated. Define exportable symbols with 'var' and use Extension.getCurrentExtension().imports to access local modules.`);
+ }
+
+ // Relative paths: './foo' or '../foo' -> extension local module
+ if (path.startsWith('./') || path.startsWith('../')) {
+ return _requireLocal(extension, path);
+ }
+
+ // GI imports: 'gi.St' -> imports.gi.St
+ if (path.startsWith('gi.')) {
+ return imports.gi[path.slice(3)];
+ }
+
+ // Cinnamon imports: 'ui.main', 'misc.util', etc.
+ let prefixes = ['ui', 'misc', 'perf'];
+ for (let prefix of prefixes) {
+ if (path.startsWith(prefix + '.')) {
+ return imports[prefix][path.slice(prefix.length + 1)];
+ }
+ }
+
+ // Bare name: try as a local module first, fall back to global
+ try {
+ return _requireLocal(extension, path);
+ } catch (e) {
+ return imports[path];
+ }
+}
+
+/**
+ * installXletImporter:
+ * @extension (Extension): The extension object
+ *
+ * Install native importer for xlet by temporarily modifying
+ * the search path.
+ */
+function installXletImporter(extension) {
+ // extension.dir is the actual directory containing the JS files,
+ // which might be a versioned subdirectory (e.g., .../uuid/6.0/)
+ // or the uuid directory itself for non-versioned xlets.
+ let parentPath = extension.dir.get_parent().get_path();
+ let dirName = extension.dir.get_basename();
+
+ let oldSearchPath = imports.searchPath.slice();
+ imports.searchPath = [parentPath];
+
+ try {
+ extension.imports = imports[dirName];
+ } catch (e) {
+ imports.searchPath = oldSearchPath;
+ throw new Error(`Failed to create importer for ${extension.uuid} at ${parentPath}/${dirName}: ${e.message}`);
+ }
+
+ imports.searchPath = oldSearchPath;
+
+ if (!extension.imports) {
+ throw new Error(`Importer is null for ${extension.uuid} at ${parentPath}/${dirName}`);
+ }
+}
+
+/**
+ * clearXletImportCache:
+ * @extension (Extension): The extension object
+ *
+ * Clear import cache to allow reloading of the xlet.
+ * Clears all cached module properties from the xlet's sub-importer.
+ */
+function clearXletImportCache(extension) {
+ if (!extension) return;
+
+ // Clear all cached modules from the xlet's importer
+ if (!extension.imports) return;
+
+ // Meta properties that should not be cleared
+ const metaProps = ['searchPath', '__moduleName__', '__parentModule__',
+ '__modulePath__', '__file__', '__init__', 'toString',
+ 'clearCache'];
+
+ try {
+ let props = Object.getOwnPropertyNames(extension.imports);
+ for (let prop of props) {
+ if (!metaProps.includes(prop)) {
+ extension.imports.clearCache(prop);
+ }
+ }
+ } catch (e) {
+ // clearCache may not be available if cjs is not updated
+ }
+}
+
+globalThis.require = xletRequire;
+
var Type = {
EXTENSION: _createExtensionType("Extension", "extensions", ExtensionSystem, {
requiredFunctions: ["init", "disable", "enable"],
@@ -146,17 +399,11 @@ function logError(message, uuid, error, state) {
}
if (state !== State.X11_ONLY) {
- error.stack = error.stack.split('\n')
- .filter(function(line) {
- return !line.match(/|wrapPromise/);
- })
- .join('\n');
-
global.logError(error);
} else {
global.logWarning(error.message);
}
-
+
// An error during initialization leads to unloading the extension again.
let extension = getExtension(uuid);
if (extension) {
@@ -182,25 +429,25 @@ function ensureFileExists(file) {
// The Extension object itself
function Extension(type, uuid) {
let extension = getExtension(uuid);
- if (extension) {
- return Promise.resolve(true);
- }
+ if (extension) return Promise.resolve(true);
+
let force = false;
if (uuid.substr(0, 1) === '!') {
uuid = uuid.replace(/^!/, '');
force = true;
}
- let dir = findExtensionDirectory(uuid, type.userDir, type.folder);
+ let dir = findExtensionDirectory(uuid, type.userDir, type.folder);
if (dir == null) {
forgetExtension(uuid, type, true);
return Promise.resolve(null);
}
+
return this._init(dir, type, uuid, force);
}
Extension.prototype = {
- _init: function(dir, type, uuid, force) {
+ _init: async function(dir, type, uuid, force) {
this.name = type.name;
this.uuid = uuid;
this.dir = dir;
@@ -211,10 +458,34 @@ Extension.prototype = {
this.iconDirectory = null;
this.meta = createMetaDummy(uuid, dir.get_path(), State.INITIALIZING);
- let isPotentialNMClientConflict = knownCinnamon4Conflicts.indexOf(uuid) > -1;
+ try {
+ this.meta = await loadMetaData({
+ state: this.meta.state,
+ path: this.meta.path,
+ uuid: uuid,
+ userDir: type.userDir,
+ folder: type.folder,
+ force: force
+ });
+
+ // Timer needs to start after the first initial I/O
+ startTime = new Date().getTime();
+
+ if (!force) {
+ this.validateMetaData();
+ }
+
+ this.dir = await findExtensionSubdirectory(this.dir);
+ this.meta.path = this.dir.get_path();
+
+ this._finishLoad(type, uuid);
+ } catch (e) {
+ this._handleLoadError(type, uuid, e);
+ }
+ },
- const finishLoad = () => {
- // Many xlets still use appletMeta/deskletMeta to get the path
+ _finishLoad: function(type, uuid) {
+ try {
type.legacyMeta[uuid] = {path: this.meta.path};
ensureFileExists(this.dir.get_child(`${this.lowerType}.js`));
@@ -226,85 +497,52 @@ Extension.prototype = {
});
}
this.loadIconDirectory(this.dir);
- // get [extension/applet/desklet].js
- return requireModule(
- `${this.meta.path}/${this.lowerType}.js`, // path
- this.meta.path, // dir,
- this.meta, // meta
- this.lowerType, // type
- true, // async
- true // returnIndex
- );
- };
-
- return loadMetaData({
- state: this.meta.state,
- path: this.meta.path,
- uuid: uuid,
- userDir: type.userDir,
- folder: type.folder,
- force: force
- }).then((meta) => {
- // Timer needs to start after the first initial I/O, otherwise every applet shows as taking 1-2 seconds to load.
- // Maybe because of how promises are wired up in CJS?
- // https://github.com/linuxmint/cjs/blob/055da399c794b0b4d76ecd7b5fabf7f960f77518/modules/_lie.js#L9
- startTime = new Date().getTime();
- this.meta = meta;
-
- if (!force) {
- this.validateMetaData();
- }
- return findExtensionSubdirectory(this.dir).then((dir) => {
- this.dir = dir;
- this.meta.path = this.dir.get_path();
+ installXletImporter(this);
+ currentlyLoadingExtension = this;
- // If an xlet has known usage of imports.gi.NMClient, we require them to have a
- // 4.0 directory. It is the only way to assume they are patched for Cinnamon 4 from here.
- if (isPotentialNMClientConflict && this.meta.path.indexOf(`/4.0`) === -1) {
- throw new Error(`Found unpatched usage of imports.gi.NMClient for ${this.lowerType} ${uuid}`);
- }
+ try {
+ this.module = this.imports[this.lowerType];
+ } finally {
+ currentlyLoadingExtension = null;
+ }
- return finishLoad();
- });
- }).then((moduleIndex) => {
- if (moduleIndex == null) {
- throw new Error(`Could not find module index: ${moduleIndex}`);
+ if (this.module == null) {
+ throw new Error(`Could not load module for ${uuid}`);
}
- this.moduleIndex = moduleIndex;
- for (let i = 0; i < type.requiredFunctions.length; i++) {
- let func = type.requiredFunctions[i];
- if (!getModuleByIndex(moduleIndex)[func]) {
+
+ for (let func of type.requiredFunctions) {
+ if (!this.module[func]) {
throw new Error(`Function "${func}" is missing`);
}
}
- // Add the extension to the global collection
extensions.push(this);
- if(!type.callbacks.finishExtensionLoad(extensions.length - 1)) {
- throw new Error(`${type.name} ${uuid}: Could not create ${this.lowerType} object.`);
+ if (!type.callbacks.finishExtensionLoad(extensions.length - 1)) {
+ throw new Error(`Could not create ${this.lowerType} object.`);
}
+
this.finalize();
Main.cinnamonDBusService.EmitXletAddedComplete(true, uuid);
- }).catch((e) => {
- // Silently fail to load xlets that aren't actually installed -
- // but no error, since the user can't do anything about it anyhow
- // (short of editing gsettings). Silent failure is consistent with
- // other reactions in Cinnamon to missing items (e.g. panel launchers
- // just don't show up if their program isn't installed, but we don't
- // remove them or anything)
- Main.cinnamonDBusService.EmitXletAddedComplete(false, uuid);
-
- if (e.cause == null || e.cause !== State.X11_ONLY) {
- Main.xlet_startup_error = true;
- }
- forgetExtension(uuid, type);
- if (e._alreadyLogged) {
- return;
- }
- logError(`Error importing ${this.lowerType}.js from ${uuid}`, uuid, e);
- });
+
+ } catch (e) {
+ this._handleLoadError(type, uuid, e);
+ }
+ },
+
+ _handleLoadError: function(type, uuid, error) {
+ Main.cinnamonDBusService.EmitXletAddedComplete(false, uuid);
+
+ if (error.cause == null || error.cause !== State.X11_ONLY) {
+ Main.xlet_startup_error = true;
+ }
+
+ forgetExtension(uuid, type);
+
+ if (!error._alreadyLogged) {
+ logError(`Error importing ${this.lowerType}.js from ${uuid}`, uuid, error);
+ }
},
finalize: function() {
@@ -579,10 +817,23 @@ function unloadExtension(uuid, type, deleteConfig = true, reload = false) {
function forgetExtension(extensionIndex, uuid, type, forgetMeta) {
if (typeof extensions[extensionIndex] !== 'undefined') {
- unloadModule(extensions[extensionIndex].moduleIndex);
- try {
- delete imports[type.folder][uuid];
- } catch (e) {}
+ let extension = extensions[extensionIndex];
+
+ // Clear module.exports cache entries for this extension
+ let pathPrefix = extension.meta.path + '/';
+ for (let key in moduleExportsCache) {
+ if (key.startsWith(pathPrefix)) {
+ delete moduleExportsCache[key];
+ }
+ }
+
+ // Clear the import cache to allow reloading (must be done before nulling references)
+ clearXletImportCache(extension);
+
+ // Clear the module reference
+ extension.module = null;
+ extension.imports = null;
+
if (forgetMeta) {
extensions[extensionIndex] = undefined;
extensions.splice(extensionIndex, 1);
@@ -661,29 +912,28 @@ function maybeAddWindowAttentionHandlerRole(meta) {
}
function loadMetaData({state, path, uuid, userDir, folder, force}) {
- return new Promise((resolve, reject) => {
+ return new Promise((resolve) => {
let dir = findExtensionDirectory(uuid, userDir, folder);
let meta;
let metadataFile = dir.get_child('metadata.json');
let oldState = state ? state : State.INITIALIZING;
let oldPath = path ? path : dir.get_path();
+
ensureFileExists(metadataFile);
+
metadataFile.load_contents_async(null, (object, result) => {
try {
let [success, json] = metadataFile.load_contents_finish(result);
if (!success) {
- reject();
- return;
+ throw new Error('Failed to load metadata');
}
meta = JSON.parse(ByteArray.toString(json));
-
maybeAddWindowAttentionHandlerRole(meta);
} catch (e) {
logError(`Failed to load/parse metadata.json`, uuid, e);
meta = createMetaDummy(uuid, oldPath, State.ERROR);
-
}
- // Store some additional crap here
+
meta.state = oldState;
meta.path = oldPath;
meta.error = '';
@@ -702,45 +952,47 @@ function loadMetaData({state, path, uuid, userDir, folder, force}) {
* equal to the current running version. If no such version is found, the
* original directory is returned.
*
- * Returns (Gio.File): directory object of the desired directory.
+ * Returns: Promise that resolves to the directory
*/
function findExtensionSubdirectory(dir) {
- return new Promise(function(resolve, reject) {
+ return new Promise((resolve) => {
dir.enumerate_children_async(
'standard::*',
Gio.FileQueryInfoFlags.NONE,
GLib.PRIORITY_DEFAULT,
null,
- function(obj, res) {
- try {
- let fileEnum = obj.enumerate_children_finish(res);
- let info;
- let largest = null;
- while ((info = fileEnum.next_file(null)) != null) {
- let fileType = info.get_file_type();
- if (fileType !== Gio.FileType.DIRECTORY) {
- continue;
- }
-
- let name = info.get_name();
- if (!name.match(/^[1-9][0-9]*\.[0-9]+(\.[0-9]+)?$/)) {
- continue;
+ (obj, res) => {
+ try {
+ let fileEnum = obj.enumerate_children_finish(res);
+ let info;
+ let largest = null;
+
+ while ((info = fileEnum.next_file(null)) != null) {
+ let fileType = info.get_file_type();
+ if (fileType !== Gio.FileType.DIRECTORY) {
+ continue;
+ }
+
+ let name = info.get_name();
+ if (!name.match(/^[1-9][0-9]*\.[0-9]+(\.[0-9]+)?$/)) {
+ continue;
+ }
+
+ if (versionLeq(name, Config.PACKAGE_VERSION) &&
+ (!largest || versionLeq(largest[0], name))) {
+ largest = [name, fileEnum.get_child(info)];
+ }
}
- if (versionLeq(name, Config.PACKAGE_VERSION) &&
- (!largest || versionLeq(largest[0], name))) {
- largest = [name, fileEnum.get_child(info)];
- }
+ fileEnum.close(null);
+ resolve(largest ? largest[1] : dir);
+ } catch (e) {
+ logError(`Error looking for extension version for ${dir.get_basename()}`,
+ 'findExtensionSubdirectory', e);
+ resolve(dir); // Fall back to original dir
}
-
- fileEnum.close(null);
- resolve(largest ? largest[1] : dir);
- } catch (e) {
- logError(`Error looking for extension version for ${dir.get_basename()} in directory ${dir}`, 'findExtensionSubdirectory', e);
- resolve(dir)
}
-
- });
+ );
});
}
diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js
index 35bb54a813..27c6f3ae1a 100644
--- a/js/ui/extensionSystem.js
+++ b/js/ui/extensionSystem.js
@@ -2,7 +2,6 @@
const Main = imports.ui.main;
const Extension = imports.ui.extension;
-const {getModuleByIndex} = imports.misc.fileUtils;
// Maps uuid -> importer object (extension directory tree)
var extensions;
@@ -33,7 +32,7 @@ function enableExtension(uuid) {
// Callback for extension.js
function prepareExtensionUnload(extension) {
try {
- getModuleByIndex(extension.moduleIndex).disable();
+ extension.module.disable();
} catch (e) {
Extension.logError('Failed to evaluate \'disable\' function on extension: ' + extension.uuid, e);
}
@@ -50,7 +49,7 @@ function prepareExtensionUnload(extension) {
// Callback for extension.js
function prepareExtensionReload(extension) {
try {
- let on_extension_reloaded = getModuleByIndex(extension.moduleIndex).on_extension_reloaded;
+ let on_extension_reloaded = extension.module.on_extension_reloaded;
if (on_extension_reloaded) on_extension_reloaded();
} catch (e) {
Extension.logError('Failed to evaluate \'on_extension_reloaded\' function on extension: ' + extension.uuid, e);
@@ -60,12 +59,12 @@ function prepareExtensionReload(extension) {
// Callback for extension.js
function finishExtensionLoad(extensionIndex) {
let extension = Extension.extensions[extensionIndex];
- if (!extension.lockRole(getModuleByIndex(extension.moduleIndex))) {
+ if (!extension.lockRole(extension.module)) {
return false;
}
try {
- getModuleByIndex(extension.moduleIndex).init(extension.meta);
+ extension.module.init(extension.meta);
} catch (e) {
Extension.logError('Failed to evaluate \'init\' function on extension: ' + extension.uuid, e);
return false;
@@ -73,7 +72,7 @@ function finishExtensionLoad(extensionIndex) {
let extensionCallbacks;
try {
- extensionCallbacks = getModuleByIndex(extension.moduleIndex).enable();
+ extensionCallbacks = extension.module.enable();
} catch (e) {
Extension.logError('Failed to evaluate \'enable\' function on extension: ' + extension.uuid, e);
return false;
diff --git a/js/ui/flashspot.js b/js/ui/flashspot.js
index 874b3ad266..34b6a5e415 100644
--- a/js/ui/flashspot.js
+++ b/js/ui/flashspot.js
@@ -1,14 +1,16 @@
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
const Clutter = imports.gi.Clutter;
+const GObject = imports.gi.GObject;
const Lightbox = imports.ui.lightbox;
const Main = imports.ui.main;
const FLASHSPOT_ANIMATION_TIME = 200; // seconds
-var Flashspot = class Flashspot extends Lightbox.Lightbox {
- constructor(area) {
- super(
+var Flashspot = GObject.registerClass(
+class Flashspot extends Lightbox.Lightbox {
+ _init(area) {
+ super._init(
Main.uiGroup,
{
inhibitEvents: true,
@@ -17,20 +19,20 @@ var Flashspot = class Flashspot extends Lightbox.Lightbox {
}
);
- this.actor.style_class = 'flashspot';
- this.actor.set_position(area.x, area.y);
+ this.style_class = 'flashspot';
+ this.set_position(area.x, area.y);
if (area.time)
- this.animation_time = area.time;
+ this.animationTime = area.time;
else
- this.animation_time = FLASHSPOT_ANIMATION_TIME;
+ this.animationTime = FLASHSPOT_ANIMATION_TIME;
}
fire() {
- this.actor.show();
- this.actor.opacity = 255;
- this.actor.ease({
+ this.show();
+ this.opacity = 255;
+ this.ease({
opacity: 0,
- duration: this.animation_time,
+ duration: this.animationTime,
animationRequired: true,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete: () => this._onFireShowComplete()
@@ -40,5 +42,5 @@ var Flashspot = class Flashspot extends Lightbox.Lightbox {
_onFireShowComplete () {
this.destroy();
}
-};
+});
diff --git a/js/ui/hotCorner.js b/js/ui/hotCorner.js
index 3bd0e4c344..cad855eac5 100755
--- a/js/ui/hotCorner.js
+++ b/js/ui/hotCorner.js
@@ -1,13 +1,15 @@
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
const Clutter = imports.gi.Clutter;
+const GLib = imports.gi.GLib;
+const GObject = imports.gi.GObject;
const Meta = imports.gi.Meta;
const St = imports.gi.St;
const Util = imports.misc.util;
const Layout = imports.ui.layout;
const Main = imports.ui.main;
-const Mainloop = imports.mainloop;
+const Ripples = imports.ui.ripples;
const HOT_CORNER_ACTIVATION_TIMEOUT = 500; // Milliseconds
const OVERVIEW_CORNERS_KEY = 'hotcorner-layout';
@@ -25,11 +27,20 @@ const LRC = 3;
//
// This class manages a "hot corner" that can toggle switching to
// overview.
-class HotCorner {
- constructor(corner_type, is_fullscreen) {
+var HotCorner = GObject.registerClass(
+class HotCorner extends Clutter.Actor {
+ _init(cornerType, isFullscreen) {
+ super._init({
+ name: 'hot-corner',
+ width: CORNER_ACTOR_SIZE,
+ height: CORNER_ACTOR_SIZE,
+ opacity: 0,
+ reactive: true,
+ });
+
this.action = null; // The action to activate when hot corner is triggered
- this.hover_delay = 0; // Hover delay activation
- this.hover_delay_id = 0; // Hover delay timer ID
+ this.hoverDelay = 0; // Hover delay activation
+ this.hoverDelayId = 0; // Hover delay timer ID
this._hoverActivationTime = 0; // Milliseconds
this._hleg = null;
@@ -37,205 +48,111 @@ class HotCorner {
const m = Main.layoutManager.primaryMonitor;
- this.actor = new Clutter.Actor({
- name: 'hot-corner',
- width: CORNER_ACTOR_SIZE,
- height: CORNER_ACTOR_SIZE,
- opacity: 0,
- reactive: true
- });
-
- if(is_fullscreen) {
- Main.layoutManager.addChrome(this.actor, {visibleInFullscreen:true});
+ if(isFullscreen) {
+ Main.layoutManager.addChrome(this, { visibleInFullscreen: true });
} else {
- Main.layoutManager.addChrome(this.actor);
+ Main.layoutManager.addChrome(this);
}
- this.actor.raise_top();
- switch (corner_type) {
+ switch (cornerType) {
case ULC:
- this._hleg = new Meta.Barrier(
- {
- display: global.display,
- x1: m.x, y1: m.y,
- x2: m.x + CORNER_FENCE_LENGTH, y2: m.y,
- directions: Meta.BarrierDirection.POSITIVE_Y
- }
- );
-
- this._vleg = new Meta.Barrier(
- {
- display: global.display,
- x1: m.x, y1: m.y,
- x2: m.x, y2: m.y + CORNER_FENCE_LENGTH,
- directions: Meta.BarrierDirection.POSITIVE_X
- }
- );
- this.actor.set_position(m.x, m.y);
+ this._hleg = new Meta.Barrier({
+ display: global.display,
+ x1: m.x, y1: m.y,
+ x2: m.x + CORNER_FENCE_LENGTH, y2: m.y,
+ directions: Meta.BarrierDirection.POSITIVE_Y
+ });
+
+ this._vleg = new Meta.Barrier({
+ display: global.display,
+ x1: m.x, y1: m.y,
+ x2: m.x, y2: m.y + CORNER_FENCE_LENGTH,
+ directions: Meta.BarrierDirection.POSITIVE_X
+ });
+ this.set_position(m.x, m.y);
break;
case URC:
- this._hleg = new Meta.Barrier(
- {
- display: global.display,
- x1: m.x + m.width - CORNER_FENCE_LENGTH, y1: m.y,
- x2: m.x + m.width, y2: m.y,
- directions: Meta.BarrierDirection.POSITIVE_Y
- }
- );
-
- this._vleg = new Meta.Barrier(
- {
- display: global.display,
- x1: m.x + m.width, y1: m.y,
- x2: m.x + m.width, y2: m.y + CORNER_FENCE_LENGTH,
- directions: Meta.BarrierDirection.NEGATIVE_X
- }
- );
- this.actor.set_position(m.x + m.width - CORNER_ACTOR_SIZE, m.y);
+ this._hleg = new Meta.Barrier({
+ display: global.display,
+ x1: m.x + m.width - CORNER_FENCE_LENGTH, y1: m.y,
+ x2: m.x + m.width, y2: m.y,
+ directions: Meta.BarrierDirection.POSITIVE_Y
+ });
+
+ this._vleg = new Meta.Barrier({
+ display: global.display,
+ x1: m.x + m.width, y1: m.y,
+ x2: m.x + m.width, y2: m.y + CORNER_FENCE_LENGTH,
+ directions: Meta.BarrierDirection.NEGATIVE_X
+ });
+ this.set_position(m.x + m.width - CORNER_ACTOR_SIZE, m.y);
break;
case LLC:
- this._hleg = new Meta.Barrier(
- {
- display: global.display,
- x1: m.x, y1: m.y + m.height,
- x2: m.x + CORNER_FENCE_LENGTH, y2: m.y + m.height,
- directions: Meta.BarrierDirection.NEGATIVE_Y
- }
- );
-
- this._vleg = new Meta.Barrier(
- {
- display: global.display,
- x1: m.x, y1: m.y + m.height - CORNER_FENCE_LENGTH,
- x2: m.x, y2: m.y + m.height,
- directions: Meta.BarrierDirection.POSITIVE_X
- }
- );
- this.actor.set_position(m.x, m.y + m.height - CORNER_ACTOR_SIZE);
+ this._hleg = new Meta.Barrier({
+ display: global.display,
+ x1: m.x, y1: m.y + m.height,
+ x2: m.x + CORNER_FENCE_LENGTH, y2: m.y + m.height,
+ directions: Meta.BarrierDirection.NEGATIVE_Y
+ });
+
+ this._vleg = new Meta.Barrier({
+ display: global.display,
+ x1: m.x, y1: m.y + m.height - CORNER_FENCE_LENGTH,
+ x2: m.x, y2: m.y + m.height,
+ directions: Meta.BarrierDirection.POSITIVE_X
+ });
+ this.set_position(m.x, m.y + m.height - CORNER_ACTOR_SIZE);
break;
case LRC:
- this._hleg = new Meta.Barrier(
- {
- display: global.display,
- x1: m.x + m.width - CORNER_FENCE_LENGTH, y1: m.y + m.height,
- x2: m.x + m.width, y2: m.y + m.height,
- directions: Meta.BarrierDirection.NEGATIVE_Y
- }
- );
-
- this._vleg = new Meta.Barrier(
- {
- display: global.display,
- x1: m.x + m.width, y1: m.y + m.height - CORNER_FENCE_LENGTH,
- x2: m.x + m.width, y2: m.y + m.height,
- directions: Meta.BarrierDirection.POSITIVE_X
- }
- );
- this.actor.set_position(m.x + m.width - CORNER_ACTOR_SIZE, m.y + m.height - CORNER_ACTOR_SIZE);
- break;
+ this._hleg = new Meta.Barrier({
+ display: global.display,
+ x1: m.x + m.width - CORNER_FENCE_LENGTH, y1: m.y + m.height,
+ x2: m.x + m.width, y2: m.y + m.height,
+ directions: Meta.BarrierDirection.NEGATIVE_Y
+ });
+
+ this._vleg = new Meta.Barrier({
+ display: global.display,
+ x1: m.x + m.width, y1: m.y + m.height - CORNER_FENCE_LENGTH,
+ x2: m.x + m.width, y2: m.y + m.height,
+ directions: Meta.BarrierDirection.POSITIVE_X
+ });
+ this.set_position(m.x + m.width - CORNER_ACTOR_SIZE, m.y + m.height - CORNER_ACTOR_SIZE);
+ break;
}
- // Construct the hot corner 'ripples'
-
// In addition to being triggered by the mouse enter event,
// the hot corner can be triggered by clicking on it. This is
// useful if the user wants to undo the effect of triggering
// the hot corner once in the hot corner.
- this.actor.connect('enter-event', () => this._onCornerEntered());
- this.actor.connect('button-release-event', () => this._onCornerClicked());
- this.actor.connect('leave-event', () => this._onCornerLeft());
-
- // Cache the three ripples instead of dynamically creating and destroying them.
- this._ripple1 = new St.Widget({
- style_class: 'ripple-box',
- opacity: 0
- });
- this._ripple2 = new St.Widget({
- style_class: 'ripple-box',
- opacity: 0
- });
- this._ripple3 = new St.Widget({
- style_class: 'ripple-box',
- opacity: 0
- });
+ this.connect('enter-event', () => this._onCornerEntered());
+ this.connect('button-release-event', () => this._onCornerClicked());
+ this.connect('leave-event', () => this._onCornerLeft());
- Main.uiGroup.add_actor(this._ripple1);
- Main.uiGroup.add_actor(this._ripple2);
- Main.uiGroup.add_actor(this._ripple3);
+ this._ripples = new Ripples.Ripples(0.5, 0.5, 'ripple-box');
+ this._ripples.addTo(Main.uiGroup);
- this._ripple1.hide();
- this._ripple2.hide();
- this._ripple3.hide();
+ this.connect('destroy', this._onDestroy.bind(this));
}
- destroy() {
+ _onDestroy() {
this._vleg = null;
this._hleg = null;
- this._ripple1.destroy();
- this._ripple2.destroy();
- this._ripple3.destroy();
- Main.layoutManager.removeChrome(this.actor)
- }
-
- _animRipple(ripple, delay, duration, startScale, startOpacity, finalScale) {
- ripple.remove_all_transitions();
- // We draw a ripple by using a source image and animating it scaling
- // outwards and fading away. We want the ripples to move linearly
- // or it looks unrealistic, but if the opacity of the ripple goes
- // linearly to zero it fades away too quickly, so we use easing
- // 'onUpdate' to give a non-linear curve to the fade-away and make
- // it more visible in the middle section.
-
- ripple._opacity = startOpacity;
-
- // Set anchor point on the center of the ripples
- ripple.set_pivot_point(0.5, 0.5);
- ripple.set_translation(-ripple.width/2, -ripple.height/2, 0);
-
- ripple.visible = true;
- ripple.opacity = 255 * Math.sqrt(startOpacity);
- ripple.scale_x = ripple.scale_y = startScale;
-
- let [x, y] = this.actor.get_transformed_position();
- ripple.x = x;
- ripple.y = y;
- ripple.ease({
- scale_x: finalScale,
- scale_y: finalScale,
- delay: delay,
- duration: duration,
- mode: Clutter.AnimationMode.LINEAR,
- onUpdate: (timeline, index) => {
- ripple._opacity = (1 - (index / duration)) * startOpacity;
- ripple.opacity = 255 * Math.sqrt(ripple._opacity);
- },
- onComplete: function() {
- ripple.visible = false;
- }
- });
+ Main.layoutManager.removeChrome(this);
+ this._ripples.destroy();
}
setProperties(properties) {
this.action = properties[0];
- this.hover_delay = properties[2] ? Number(properties[2]) : 0;
+ this.hoverDelay = properties[2] ? Number(properties[2]) : 0;
}
rippleAnimation() {
- // Show three concentric ripples expanding outwards; the exact
- // parameters were found by trial and error, so don't look
- // for them to make perfect sense mathematically
-
- this._ripple1.show();
- this._ripple2.show();
- this._ripple3.show();
-
- // delay duration scale opacity fscale
- this._animRipple(this._ripple1, 0, 830, 0.25, 1.0, 1.5);
- this._animRipple(this._ripple2, 50, 1000, 0.0, 0.7, 1.25);
- this._animRipple(this._ripple3, 350, 1000, 0.0, 0.3, 1);
+ let [x, y] = this.get_transformed_position();
+ this._ripples.playAnimation(x, y);
}
runAction(timestamp) {
@@ -257,33 +174,33 @@ class HotCorner {
}
_onCornerEntered() {
- if (this.hover_delay_id > 0) {
- Mainloop.source_remove(this.hover_delay_id);
- this.hover_delay_id = 0;
+ if (this.hoverDelayId > 0) {
+ GLib.source_remove(this.hoverDelayId);
+ this.hoverDelayId = 0;
}
/* Get the timestamp outside the timeout handler because
global.get_current_time() can only be called within the
scope of an event handler or it will return 0 */
- let timestamp = global.get_current_time() + this.hover_delay;
- this.hover_delay_id = Mainloop.timeout_add(this.hover_delay, () => {
+ let timestamp = global.get_current_time() + this.hoverDelay;
+ this.hoverDelayId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, this.hoverDelay, () => {
if (this.shouldRunAction(timestamp, false)) {
this._hoverActivationTime = timestamp;
this.rippleAnimation();
this.runAction(timestamp);
}
- this.hover_delay_id = 0;
- return false;
+ this.hoverDelayId = 0;
+ return GLib.SOURCE_REMOVE;
});
return Clutter.EVENT_PROPAGATE;
}
_onCornerClicked() {
- if (this.hover_delay_id > 0) {
- Mainloop.source_remove(this.hover_delay_id);
- this.hover_delay_id = 0;
+ if (this.hoverDelayId > 0) {
+ GLib.source_remove(this.hoverDelayId);
+ this.hoverDelayId = 0;
}
let timestamp = global.get_current_time();
@@ -296,9 +213,9 @@ class HotCorner {
}
_onCornerLeft() {
- if (this.hover_delay_id > 0) {
- Mainloop.source_remove(this.hover_delay_id);
- this.hover_delay_id = 0;
+ if (this.hoverDelayId > 0) {
+ GLib.source_remove(this.hoverDelayId);
+ this.hoverDelayId = 0;
}
// Consume event
return Clutter.EVENT_STOP;
@@ -320,7 +237,7 @@ class HotCorner {
return true;
}
-};
+});
var HotCornerManager = class {
constructor() {
@@ -332,7 +249,7 @@ var HotCornerManager = class {
update() {
let options = global.settings.get_strv(OVERVIEW_CORNERS_KEY);
- let is_fullscreen = global.settings.get_boolean(CORNERS_FULLSCREEN_KEY);
+ let isFullscreen = global.settings.get_boolean(CORNERS_FULLSCREEN_KEY);
if (options.length != 4) {
global.logError(_("Invalid overview options: Incorrect number of corners"));
@@ -355,7 +272,7 @@ var HotCornerManager = class {
elements.unshift(cmd);
}
- if(is_fullscreen === true) {
+ if(isFullscreen === true) {
this.corners[i] = new HotCorner(i, true);
} else {
this.corners[i] = new HotCorner(i);
diff --git a/js/ui/keybindings.js b/js/ui/keybindings.js
index 3f7d86f386..f3c1763c93 100644
--- a/js/ui/keybindings.js
+++ b/js/ui/keybindings.js
@@ -5,6 +5,7 @@ const GLib = imports.gi.GLib;
const Lang = imports.lang;
const Util = imports.misc.util;
const Meta = imports.gi.Meta;
+const Cinnamon = imports.gi.Cinnamon;
const AppletManager = imports.ui.appletManager;
const DeskletManager = imports.ui.deskletManager;
@@ -17,6 +18,19 @@ const CUSTOM_KEYS_SCHEMA = "org.cinnamon.desktop.keybindings.custom-keybinding";
const MEDIA_KEYS_SCHEMA = "org.cinnamon.desktop.keybindings.media-keys";
+const REPEATABLE_MEDIA_KEYS = [
+ MK.VOLUME_UP,
+ MK.VOLUME_UP_QUIET,
+ MK.VOLUME_DOWN,
+ MK.VOLUME_DOWN_QUIET,
+ MK.SCREEN_BRIGHTNESS_UP,
+ MK.SCREEN_BRIGHTNESS_DOWN,
+ MK.KEYBOARD_BRIGHTNESS_UP,
+ MK.KEYBOARD_BRIGHTNESS_DOWN,
+ MK.REWIND,
+ MK.FORWARD,
+];
+
const OBSOLETE_MEDIA_KEYS = [
MK.VIDEO_OUT,
MK.ROTATE_VIDEO
@@ -62,6 +76,9 @@ KeybindingManager.prototype = {
this.media_key_settings = new Gio.Settings({ schema_id: MEDIA_KEYS_SCHEMA });
this.media_key_settings.connect("changed", Lang.bind(this, this.setup_media_keys));
+
+ this.screensaver_settings = new Gio.Settings({ schema_id: "org.cinnamon.desktop.screensaver" });
+
this.setup_media_keys();
},
@@ -70,10 +87,10 @@ KeybindingManager.prototype = {
this.setup_custom_keybindings();
},
- addHotKey: function(name, bindings_string, callback) {
+ addHotKey: function(name, bindings_string, callback, flags, allowedModes) {
if (!bindings_string)
return false;
- return this.addHotKeyArray(name, bindings_string.split("::"), callback);
+ return this.addHotKeyArray(name, bindings_string.split("::"), callback, flags, allowedModes);
},
_makeXletKey: function(xlet, name, binding) {
@@ -108,7 +125,7 @@ KeybindingManager.prototype = {
* }
*/
- addXletHotKey: function(xlet, name, bindings_string, callback) {
+ addXletHotKey: function(xlet, name, bindings_string, callback, flags, allowedModes) {
this._removeMatchingXletBindings(xlet, name);
if (!bindings_string)
@@ -131,7 +148,7 @@ KeybindingManager.prototype = {
xlet_set.set(instanceId, callback);
- this._queueCommitXletHotKey(xlet_key, binding, xlet_set);
+ this._queueCommitXletHotKey(xlet_key, binding, xlet_set, flags, allowedModes);
}
},
@@ -225,7 +242,7 @@ KeybindingManager.prototype = {
this._removeMatchingXletBindings(xlet, name);
},
- _queueCommitXletHotKey: function(xlet_key, binding, xlet_set) {
+ _queueCommitXletHotKey: function(xlet_key, binding, xlet_set, flags, allowedModes) {
let id = xlet_set.get("commitTimeoutId") ?? 0;
if (id > 0) {
@@ -233,7 +250,7 @@ KeybindingManager.prototype = {
}
id = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
- this.addHotKeyArray(xlet_key, [binding], this._xletCallback.bind(this, xlet_key));
+ this.addHotKeyArray(xlet_key, [binding], this._xletCallback.bind(this, xlet_key), flags, allowedModes);
xlet_set.set("commitTimeoutId", 0);
return GLib.SOURCE_REMOVE;
});
@@ -253,7 +270,13 @@ KeybindingManager.prototype = {
return [Meta.KeyBindingAction.NONE, undefined];
},
- addHotKeyArray: function(name, bindings, callback) {
+ getBindingById: function(action_id) {
+ return this.bindings.get(action_id);
+ },
+
+ addHotKeyArray: function(name, bindings, callback,
+ flags=Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
+ allowedModes=Cinnamon.ActionMode.NORMAL) {
let [existing_action_id, entry] = this._lookupEntry(name);
if (entry !== undefined) {
@@ -278,17 +301,18 @@ KeybindingManager.prototype = {
return true;
}
- action_id = global.display.add_custom_keybinding(name, bindings, callback);
- // log(`set keybinding: ${name}, bindings: ${bindings} - action id: ${action_id}`);
+ action_id = global.display.add_custom_keybinding_full(name, bindings, flags, callback);
+ // log(`set keybinding: ${name}, bindings: ${bindings}, flags: ${flags}, allowedModes: ${allowedModes} - action id: ${action_id}`);
if (action_id === Meta.KeyBindingAction.NONE) {
global.logError("Warning, unable to bind hotkey with name '" + name + "'. The selected keybinding could already be in use.");
return false;
}
this.bindings.set(action_id, {
- "name" : name,
- "bindings": bindings,
- "callback": callback
+ "name" : name,
+ "bindings" : bindings,
+ "callback" : callback,
+ "allowedModes": allowedModes
});
return true;
@@ -335,39 +359,70 @@ KeybindingManager.prototype = {
},
setup_media_keys: function() {
+ // Media keys before SEPARATOR work in all modes (global keys)
+ // These should work during lock screen, unlock dialog, etc.
+ let globalModes = Cinnamon.ActionMode.NORMAL | Cinnamon.ActionMode.OVERVIEW |
+ Cinnamon.ActionMode.LOCK_SCREEN | Cinnamon.ActionMode.UNLOCK_SCREEN |
+ Cinnamon.ActionMode.SYSTEM_MODAL | Cinnamon.ActionMode.LOOKING_GLASS |
+ Cinnamon.ActionMode.POPUP;
+
for (let i = 0; i < MK.SEPARATOR; i++) {
if (is_obsolete_mk(i)) {
continue;
}
+ let flags = REPEATABLE_MEDIA_KEYS.includes(i)
+ ? Meta.KeyBindingFlags.NONE
+ : Meta.KeyBindingFlags.IGNORE_AUTOREPEAT;
+
let bindings = this.media_key_settings.get_strv(CinnamonDesktop.desktop_get_media_key_string(i));
this.addHotKeyArray("media-keys-" + i.toString(),
bindings,
- Lang.bind(this, this.on_global_media_key_pressed, i));
+ Lang.bind(this, this.on_media_key_pressed, i),
+ flags,
+ globalModes);
}
+ // Media keys after SEPARATOR only work in normal mode
for (let i = MK.SEPARATOR + 1; i < MK.LAST; i++) {
if (is_obsolete_mk(i)) {
continue;
}
+ let flags = REPEATABLE_MEDIA_KEYS.includes(i)
+ ? Meta.KeyBindingFlags.NONE
+ : Meta.KeyBindingFlags.IGNORE_AUTOREPEAT;
+
let bindings = this.media_key_settings.get_strv(CinnamonDesktop.desktop_get_media_key_string(i));
this.addHotKeyArray("media-keys-" + i.toString(),
bindings,
- Lang.bind(this, this.on_media_key_pressed, i));
+ Lang.bind(this, this.on_media_key_pressed, i),
+ flags,
+ Cinnamon.ActionMode.NORMAL);
}
return true;
},
- on_global_media_key_pressed: function(display, window, kb, action) {
- // log(`global media key ${display}, ${window}, ${kb}, ${action}`);
- this._proxy.HandleKeybindingRemote(action);
- },
-
on_media_key_pressed: function(display, window, kb, action) {
- // log(`media key ${display}, ${window}, ${kb}, ${action}`);
- if (Main.modalCount == 0 && !Main.overview.visible && !Main.expo.visible)
- this._proxy.HandleKeybindingRemote(action);
+ let [, entry] = this._lookupEntry("media-keys-" + action.toString());
+ if (Main._shouldFilterKeybinding(entry))
+ return;
+
+ // Check if this is the screensaver key and internal screensaver is enabled
+ if (action === MK.SCREENSAVER && global.settings.get_boolean('internal-screensaver-enabled')) {
+ // If a custom screensaver is configured, skip internal handling and
+ // let csd-media-keys run cinnamon-screensaver-command instead.
+ if (!this.screensaver_settings.get_string('custom-screensaver-command').trim()) {
+ GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
+ Main.lockScreen(false);
+ return GLib.SOURCE_REMOVE;
+ });
+ return;
+ }
+ }
+
+ // Otherwise, forward to csd-media-keys (or other handler)
+ this._proxy.HandleKeybindingRemote(action);
},
invoke_keybinding_action_by_id: function(id) {
diff --git a/js/ui/keyboardManager.js b/js/ui/keyboardManager.js
index c4ed55a3ae..a09fbbc4d1 100644
--- a/js/ui/keyboardManager.js
+++ b/js/ui/keyboardManager.js
@@ -235,6 +235,7 @@ var SubscriptableFlagIcon = GObject.registerClass({
this._subscript = null;
this._file = null;
this._image = null;
+ this._loadHandle = 0;
super._init({
style_class: 'input-source-switcher-flag-icon',
@@ -246,15 +247,18 @@ var SubscriptableFlagIcon = GObject.registerClass({
this._imageBin = new St.Bin({ y_align: Clutter.ActorAlign.CENTER });
this.add_child(this._imageBin);
- this._drawingArea = new St.DrawingArea({});
+ this._drawingArea = new St.DrawingArea({
+ x_expand: true,
+ y_expand: true,
+ });
this._drawingArea.connect('repaint', this._drawingAreaRepaint.bind(this));
this.add_child(this._drawingArea);
- this.connect("allocation-changed", () => {
- GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
+ this.connect('allocation-changed', () => {
+ if (this._image == null) {
this._load_file();
- });
+ }
});
}
@@ -281,26 +285,22 @@ var SubscriptableFlagIcon = GObject.registerClass({
}
try {
- St.TextureCache.get_default().load_image_from_file_async(
+ this._loadHandle = St.TextureCache.get_default().load_image_from_file_async(
this._file.get_path(),
-1, this.get_height(),
(cache, handle, actor) => {
- this._image = actor;
- let constraint = new Clutter.BindConstraint({
- source: actor,
- coordinate: Clutter.BindCoordinate.ALL
- })
+ if (handle !== this._loadHandle) {
+ return;
+ }
- this._drawingArea.add_constraint(constraint);
+ this._image = actor;
this._imageBin.set_child(actor);
+ this._drawingArea.queue_repaint();
}
);
-
} catch (e) {
global.logError(e);
}
-
- this._drawingArea.queue_relayout();
}
_drawingAreaRepaint(area) {
@@ -310,22 +310,13 @@ var SubscriptableFlagIcon = GObject.registerClass({
const cr = area.get_context();
const [w, h] = area.get_surface_size();
- const surf_w = this._image.width;
- const surf_h = this._image.height;
cr.save();
- // // Debugging...
-
- // cr.setSourceRGBA(1.0, 1.0, 1.0, .2);
- // cr.rectangle(0, 0, w, h);
- // cr.fill();
- // cr.save()
-
if (this._subscript != null) {
- let x = surf_w / 2;
+ let x = w / 2;
let width = x;
- let y = surf_h / 2;
+ let y = h / 2;
let height = y;
cr.setSourceRGBA(0.0, 0.0, 0.0, 0.5);
cr.rectangle(x, y, width, height);
@@ -963,7 +954,7 @@ var InputSourceManager = class {
style_class: actorClass,
file: file,
subscript: source.dupeId > 0 ? String(source.dupeId) : null,
- height: size,
+ height: size * global.ui_scale,
});
}
diff --git a/js/ui/keyringPrompt.js b/js/ui/keyringPrompt.js
index 14970fa2f7..6a4cd65863 100644
--- a/js/ui/keyringPrompt.js
+++ b/js/ui/keyringPrompt.js
@@ -87,7 +87,7 @@ class KeyringDialog extends ModalDialog.ModalDialog {
passwordBox.add_child(warningBox);
content.add_child(passwordBox);
- this._choice = new CheckBox.CheckBox2();
+ this._choice = new CheckBox.CheckBox();
this.prompt.bind_property('choice-label', this._choice.getLabelActor(),
'text', GObject.BindingFlags.SYNC_CREATE);
this.prompt.bind_property('choice-chosen', this._choice,
diff --git a/js/ui/layout.js b/js/ui/layout.js
index 9997325a8b..e3ad10ca7b 100644
--- a/js/ui/layout.js
+++ b/js/ui/layout.js
@@ -9,14 +9,11 @@ const Cinnamon = imports.gi.Cinnamon;
const GObject = imports.gi.GObject;
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
-const Lang = imports.lang;
const Mainloop = imports.mainloop;
const Meta = imports.gi.Meta;
-const Signals = imports.signals;
const St = imports.gi.St;
const Main = imports.ui.main;
const Params = imports.misc.params;
-const EdgeFlip = imports.ui.edgeFlip;
const HotCorner = imports.ui.hotCorner;
const DeskletManager = imports.ui.deskletManager;
const Panel = imports.ui.panel;
@@ -181,24 +178,20 @@ var MonitorConstraint = GObject.registerClass({
}
});
-function Monitor(index, geometry, name) {
- this._init(index, geometry, name);
-}
-
-Monitor.prototype = {
- _init: function(index, geometry, name) {
+class Monitor {
+ constructor(index, geometry, name) {
this.index = index;
this.x = geometry.x;
this.y = geometry.y;
this.width = geometry.width;
this.height = geometry.height;
this.name = name;
- },
+ }
get inFullscreen() {
return global.display.get_monitor_in_fullscreen(this.index);
}
-};
+}
var UiActor = GObject.registerClass(
class UiActor extends St.Widget {
@@ -266,70 +259,68 @@ class UiActor extends St.Widget {
* Creates and manages the Chrome container which holds
* all of the Cinnamon UI actors.
*/
-function LayoutManager() {
- this._init.apply(this, arguments);
-}
+var LayoutManager = GObject.registerClass({
+ Signals: {
+ 'monitors-changed': {},
+ 'keyboard-visible-changed': { param_types: [GObject.TYPE_BOOLEAN] },
+ },
+}, class LayoutManager extends GObject.Object {
+ _init() {
+ super._init();
-LayoutManager.prototype = {
- _init: function () {
this._rtl = (St.Widget.get_default_direction() == St.TextDirection.RTL);
this.monitors = [];
this.primaryMonitor = null;
this.primaryIndex = -1;
this.hotCornerManager = null;
- this.edgeRight = null;
- this.edgeLeft = null;
this._chrome = new Chrome(this);
- this.enabledEdgeFlip = global.settings.get_boolean("enable-edge-flip");
- this.edgeFlipDelay = global.settings.get_int("edge-flip-delay");
-
- this.keyboardBox = new St.Widget({ name: 'keyboardBox',
- layout_manager: new Clutter.BinLayout(),
- important: true,
- reactive: true,
- track_hover: true });
+ this.keyboardBox = new St.Widget({
+ name: 'keyboardBox',
+ layout_manager: new Clutter.BinLayout(),
+ important: true,
+ reactive: true,
+ track_hover: true,
+ });
this.keyboardBox.hide();
this._keyboardIndex = -1;
- this.addChrome(this.keyboardBox, { visibleInFullscreen: true, affectsStruts: false });
+ this.addChrome(this.keyboardBox, {
+ visibleInFullscreen: true,
+ affectsStruts: false
+ });
this._keyboardHeightNotifyId = 0;
this._monitorsChanged();
- global.settings.connect("changed::enable-edge-flip", Lang.bind(this, this._onEdgeFlipChanged));
- global.settings.connect("changed::edge-flip-delay", Lang.bind(this, this._onEdgeFlipChanged));
- Meta.MonitorManager.get().connect('monitors-changed', Lang.bind(this, this._monitorsChanged));
- },
-
- _onEdgeFlipChanged: function(){
- this.enabledEdgeFlip = global.settings.get_boolean("enable-edge-flip");
- this.edgeFlipDelay = global.settings.get_int("edge-flip-delay");
- this.edgeRight.enabled = this.enabledEdgeFlip;
- this.edgeRight.delay = this.edgeFlipDelay;
- this.edgeLeft.enabled = this.enabledEdgeFlip;
- this.edgeLeft.delay = this.edgeFlipDelay;
- },
+ Meta.MonitorManager.get().connect('monitors-changed', this._monitorsChanged.bind(this));
+ }
// This is called by Main after everything else is constructed;
// Certain functions need to access other Main elements that do
// not exist yet when the LayoutManager was constructed.
- init: function() {
+ init() {
this._chrome.init();
- this.edgeRight = new EdgeFlip.EdgeFlipper(St.Side.RIGHT, Main.wm.actionFlipWorkspaceRight);
- this.edgeLeft = new EdgeFlip.EdgeFlipper(St.Side.LEFT, Main.wm.actionFlipWorkspaceLeft);
-
- this.edgeRight.enabled = this.enabledEdgeFlip;
- this.edgeRight.delay = this.edgeFlipDelay;
- this.edgeLeft.enabled = this.enabledEdgeFlip;
- this.edgeLeft.delay = this.edgeFlipDelay;
-
this.hotCornerManager = new HotCorner.HotCornerManager();
- },
- _toggleExpo: function() {
+ // Create container for screen shield (above all other UI)
+ this.screenShieldGroup = new St.Widget({
+ name: 'screenShieldGroup',
+ visible: false,
+ clip_to_allocation: true,
+ layout_manager: new Clutter.BinLayout()
+ });
+ this.screenShieldGroup.add_constraint(new Clutter.BindConstraint({
+ source: global.stage,
+ coordinate: Clutter.BindCoordinate.ALL
+ }));
+ global.stage.add_actor(this.screenShieldGroup);
+ this.screenShieldGroup.raise_top();
+ }
+
+ _toggleExpo() {
if (Main.expo.animationInProgress)
return;
@@ -338,9 +329,9 @@ LayoutManager.prototype = {
Main.overview.hide();
}
Main.expo.toggle();
- },
+ }
- _updateMonitors: function() {
+ _updateMonitors() {
this.monitors = [];
let nMonitors = global.display.get_n_monitors();
for (let i = 0; i < nMonitors; i++) {
@@ -353,22 +344,22 @@ LayoutManager.prototype = {
this.primaryIndex = global.display.get_primary_monitor();
this.primaryMonitor = this.monitors[this.primaryIndex];
- },
+ }
- _updateBoxes: function() {
+ _updateBoxes() {
if (this.hotCornerManager)
this.hotCornerManager.update();
this._chrome._queueUpdateRegions();
this.keyboardIndex = this.primaryIndex;
- },
+ }
- _monitorsChanged: function() {
+ _monitorsChanged() {
this._updateMonitors();
this._updateBoxes();
this._updateKeyboardBox()
this.emit('monitors-changed');
- },
+ }
get focusIndex() {
let i = 0;
@@ -377,24 +368,26 @@ LayoutManager.prototype = {
else if (global.display.focus_window != null)
i = global.display.focus_window.get_monitor();
return i;
- },
+ }
get focusMonitor() {
return this.monitors[this.focusIndex];
- },
+ }
get currentMonitor() {
let index = global.display.get_current_monitor();
return Main.layoutManager.monitors[index];
- },
+ }
- _prepareStartupAnimation: function() {
+ _prepareStartupAnimation() {
// During the initial transition, add a simple actor to block all events,
// so they don't get delivered to X11 windows that have been transformed.
- this._coverPane = new Clutter.Actor({ opacity: 0,
- width: global.screen_width,
- height: global.screen_height,
- reactive: true });
+ this._coverPane = new Clutter.Actor({
+ opacity: 0,
+ width: global.screen_width,
+ height: global.screen_height,
+ reactive: true,
+ });
this.addChrome(this._coverPane);
// We need to force an update of the regions now before we scale
@@ -408,24 +401,24 @@ LayoutManager.prototype = {
this.startupAnimation = new StartupAnimation.Animation(this.primaryMonitor,
()=>this._startupAnimationComplete());
this._chrome.updateRegions();
- },
+ }
- _doStartupAnimation: function() {
+ _doStartupAnimation() {
// Don't animate the strut
this._chrome.freezeUpdateRegions();
this.startupAnimation.run();
- },
+ }
- _startupAnimationComplete: function() {
+ _startupAnimationComplete() {
global.stage.show_cursor();
this.removeChrome(this._coverPane);
this._coverPane = null;
this._chrome.thawUpdateRegions();
Main.setRunState(Main.RunState.RUNNING);
- },
+ }
- _updateKeyboardBox: function() {
+ _updateKeyboardBox() {
if (Main.panelManager == null || Main.virtualKeyboardManager == null) {
return;
}
@@ -468,22 +461,22 @@ LayoutManager.prototype = {
this.keyboardBox.set_position(kb_x, kb_y);
this.keyboardBox.set_size(kb_width, kb_height);
- },
+ }
get keyboardMonitor() {
return this.monitors[this.keyboardIndex];
- },
+ }
set keyboardIndex(v) {
this._keyboardIndex = v;
this._updateKeyboardBox();
- },
+ }
get keyboardIndex() {
return this._keyboardIndex;
- },
+ }
- showKeyboard: function() {
+ showKeyboard() {
this.keyboardBox.opacity = 0;
this.keyboardBox.show();
this.keyboardBox.remove_all_transitions();
@@ -496,15 +489,15 @@ LayoutManager.prototype = {
this._showKeyboardComplete();
}
});
- },
+ }
- _showKeyboardComplete: function() {
+ _showKeyboardComplete() {
this._chrome.modifyActorParams(this.keyboardBox, { affectsStruts: true });
this._chrome._queueUpdateRegions();
this.emit('keyboard-visible-changed', true);
- },
+ }
- hideKeyboard: function(immediate) {
+ hideKeyboard(immediate) {
this.keyboardBox.remove_all_transitions();
this._chrome.modifyActorParams(this.keyboardBox, { affectsStruts: false });
this._chrome._queueUpdateRegions();
@@ -519,12 +512,12 @@ LayoutManager.prototype = {
});
this.emit('keyboard-visible-changed', false);
- },
+ }
- _hideKeyboardComplete: function() {
+ _hideKeyboardComplete() {
this.keyboardBox.hide();
this.keyboardBox.opacity = 255;
- },
+ }
/**
* updateChrome:
@@ -536,12 +529,12 @@ LayoutManager.prototype = {
* Use with care as this is already frequently updated, and can reduce performance
* if called unnecessarily.
*/
- updateChrome: function(doVisibility) {
+ updateChrome(doVisibility) {
if (doVisibility === true)
this._chrome._updateVisibility();
else
this._chrome._queueUpdateRegions();
- },
+ }
/**
* addChrome:
@@ -567,9 +560,9 @@ LayoutManager.prototype = {
* If %visibleInFullscreen in @params is %true, the actor will be
* visible even when a fullscreen window should be covering it.
*/
- addChrome: function(actor, params) {
+ addChrome(actor, params) {
this._chrome.addActor(actor, params);
- },
+ }
/**
* trackChrome:
@@ -589,9 +582,9 @@ LayoutManager.prototype = {
* a %visibleInFullscreen child of a non-%visibleInFullscreen
* parent).
*/
- trackChrome: function(actor, params) {
+ trackChrome(actor, params) {
this._chrome.trackActor(actor, params);
- },
+ }
/**
* untrackChrome:
@@ -599,9 +592,9 @@ LayoutManager.prototype = {
*
* Undoes the effect of trackChrome()
*/
- untrackChrome: function(actor) {
+ untrackChrome(actor) {
this._chrome.untrackActor(actor);
- },
+ }
/**
* removeChrome:
@@ -609,9 +602,9 @@ LayoutManager.prototype = {
*
* Removes the actor from the chrome
*/
- removeChrome: function(actor) {
+ removeChrome(actor) {
this._chrome.removeActor(actor);
- },
+ }
/**
* findMonitorForActor:
@@ -622,9 +615,9 @@ LayoutManager.prototype = {
*
* Returns (Layout.Monitor): the monitor
*/
- findMonitorForActor: function(actor) {
+ findMonitorForActor(actor) {
return this._chrome.findMonitorForActor(actor);
- },
+ }
/**
* findMonitorIndexForActor
@@ -636,14 +629,14 @@ LayoutManager.prototype = {
*
* Returns (number): the monitor index
*/
- findMonitorIndexForActor: function(actor) {
+ findMonitorIndexForActor(actor) {
return this._chrome.findMonitorIndexForActor(actor);
- },
+ }
- findMonitorIndexAt: function(x, y) {
+ findMonitorIndexAt(x, y) {
let [index, monitor] = this._chrome._findMonitorForRect(x, y, 1, 1)
return index;
- },
+ }
/**
* isTrackingChrome:
@@ -653,9 +646,9 @@ LayoutManager.prototype = {
*
* Returns (boolean): whether the actor is currently tracked
*/
- isTrackingChrome: function(actor) {
+ isTrackingChrome(actor) {
return this._chrome._findActor(actor) != -1;
- },
+ }
/**
* getWindowAtPointer:
@@ -665,7 +658,7 @@ LayoutManager.prototype = {
*
* Returns (Meta.Window): the MetaWindow under the pointer, or null if none found
*/
- getWindowAtPointer: function() {
+ getWindowAtPointer() {
let [pointerX, pointerY] = global.get_pointer();
let workspace = global.workspace_manager.get_active_workspace();
let windows = workspace.list_unobscured_windows();
@@ -682,10 +675,7 @@ LayoutManager.prototype = {
return null;
}
-};
-Signals.addSignalMethods(LayoutManager.prototype);
-
-
+});
// This manages Cinnamon "chrome"; the UI that's visible in the
// normal mode (ie, outside the Overview), that surrounds the main
@@ -698,12 +688,8 @@ const defaultParams = {
doNotAdd: false
};
-function Chrome() {
- this._init.apply(this, arguments);
-}
-
-Chrome.prototype = {
- _init: function(layoutManager) {
+var Chrome = class {
+ constructor(layoutManager) {
this._layoutManager = layoutManager;
this._monitors = [];
@@ -716,35 +702,30 @@ Chrome.prototype = {
this._trackedActors = [];
- this._layoutManager.connect('monitors-changed',
- Lang.bind(this, this._relayout));
- global.display.connect('restacked',
- Lang.bind(this, this._windowsRestacked));
- global.display.connect('in-fullscreen-changed', Lang.bind(this, this._updateVisibility));
- global.window_manager.connect('switch-workspace', Lang.bind(this, this._queueUpdateRegions));
+ this._layoutManager.connect('monitors-changed', this._relayout.bind(this));
+ global.display.connect('restacked', this._windowsRestacked.bind(this));
+ global.display.connect('in-fullscreen-changed', this._updateVisibility.bind(this));
+ global.window_manager.connect('switch-workspace', this._queueUpdateRegions.bind(this));
// Need to update struts on new workspaces when they are added
- global.workspace_manager.connect('notify::n-workspaces',
- Lang.bind(this, this._queueUpdateRegions));
+ global.workspace_manager.connect('notify::n-workspaces', this._queueUpdateRegions.bind(this));
this._relayout();
- },
+ }
- init: function() {
- Main.overview.connect('showing',
- Lang.bind(this, this._overviewShowing));
- Main.overview.connect('hidden',
- Lang.bind(this, this._overviewHidden));
- },
+ init() {
+ Main.overview.connect('showing', this._overviewShowing.bind(this));
+ Main.overview.connect('hidden', this._overviewHidden.bind(this));
+ }
- addActor: function(actor, params) {
+ addActor(actor, params) {
let actorData = Params.parse(params, defaultParams);
if (actorData.addToWindowgroup) global.window_group.add_actor(actor);
else if (!actorData.doNotAdd) Main.uiGroup.add_actor(actor);
this._trackActor(actor, params);
- },
+ }
- trackActor: function(actor, params) {
+ trackActor(actor, params) {
let ancestor = actor.get_parent();
let index = this._findActor(ancestor);
while (ancestor && index == -1) {
@@ -764,13 +745,13 @@ Chrome.prototype = {
}
this._trackActor(actor, params);
- },
+ }
- untrackActor: function(actor) {
+ untrackActor(actor) {
this._untrackActor(actor);
- },
+ }
- removeActor: function(actor) {
+ removeActor(actor) {
let i = this._findActor(actor);
if (i == -1)
@@ -780,18 +761,18 @@ Chrome.prototype = {
if (actorData.addToWindowgroup) global.window_group.remove_child(actor);
else Main.uiGroup.remove_child(actor);
this._untrackActor(actor);
- },
+ }
- _findActor: function(actor) {
+ _findActor(actor) {
for (let i = 0; i < this._trackedActors.length; i++) {
let actorData = this._trackedActors[i];
if (actorData.actor == actor)
return i;
}
return -1;
- },
+ }
- modifyActorParams: function(actor, params) {
+ modifyActorParams(actor, params) {
let index = this._findActor(actor);
if (index == -1)
throw new Error('could not find actor in chrome');
@@ -799,9 +780,9 @@ Chrome.prototype = {
this._trackedActors[index][i] = params[i];
}
this._queueUpdateRegions();
- },
+ }
- _trackActor: function(actor, params) {
+ _trackActor(actor, params) {
if (this._findActor(actor) != -1)
throw new Error('trying to re-track existing chrome actor');
@@ -809,39 +790,32 @@ Chrome.prototype = {
actorData.actor = actor;
if (actorData.addToWindowgroup) actorData.isToplevel = actor.get_parent() == global.window_group;
else actorData.isToplevel = actor.get_parent() == Main.uiGroup;
- actorData.visibleId = actor.connect('notify::visible',
- Lang.bind(this, this._queueUpdateRegions));
- actorData.allocationId = actor.connect('notify::allocation',
- Lang.bind(this, this._queueUpdateRegions));
- actorData.parentSetId = actor.connect('parent-set',
- Lang.bind(this, this._actorReparented));
+ actor.connectObject(
+ 'notify::visible', this._queueUpdateRegions.bind(this),
+ 'notify::allocation', this._queueUpdateRegions.bind(this),
+ 'parent-set', this._actorReparented.bind(this),
+ 'destroy', this._untrackActor.bind(this), this);
// Note that destroying actor unsets its parent, but does not emit
// parent-set during destruction.
// https://gitlab.gnome.org/GNOME/mutter/-/commit/f376a318ba90fc29d3d661df4f55698459f31cfa
- actorData.destroyId = actor.connect('destroy',
- Lang.bind(this, this._untrackActor));
this._trackedActors.push(actorData);
this._queueUpdateRegions();
- },
+ }
- _untrackActor: function(actor) {
+ _untrackActor(actor) {
let i = this._findActor(actor);
if (i == -1)
return;
- let actorData = this._trackedActors[i];
this._trackedActors.splice(i, 1);
- actor.disconnect(actorData.visibleId);
- actor.disconnect(actorData.allocationId);
- actor.disconnect(actorData.parentSetId);
- actor.disconnect(actorData.destroyId);
+ actor.disconnectObject(this);
this._queueUpdateRegions();
- },
+ }
- _actorReparented: function(actor, oldParent) {
+ _actorReparented(actor, oldParent) {
let i = this._findActor(actor);
if (i == -1)
return;
@@ -854,9 +828,9 @@ Chrome.prototype = {
if (actorData.addToWindowgroup) actorData.isToplevel = (newParent == global.window_group);
else actorData.isToplevel = (newParent == Main.uiGroup);
}
- },
+ }
- _updateVisibility: function() {
+ _updateVisibility() {
for (let i = 0; i < this._trackedActors.length; i++) {
let actorData = this._trackedActors[i], visible;
if (!actorData.isToplevel)
@@ -879,7 +853,7 @@ Chrome.prototype = {
visible = true;
else {
let monitor = this.findMonitorForActor(actorData.actor);
-
+
if (!actorData.visibleInFullscreen && monitor && monitor.inFullscreen)
visible = false;
else
@@ -888,26 +862,26 @@ Chrome.prototype = {
Main.uiGroup.set_skip_paint(actorData.actor, !visible);
}
this._queueUpdateRegions();
- },
+ }
- _overviewShowing: function() {
+ _overviewShowing() {
this._inOverview = true;
this._updateVisibility();
- },
+ }
- _overviewHidden: function() {
+ _overviewHidden() {
this._inOverview = false;
this._updateVisibility();
- },
+ }
- _relayout: function() {
+ _relayout() {
this._monitors = this._layoutManager.monitors;
this._primaryMonitor = this._layoutManager.primaryMonitor;
this._primaryIndex = this._layoutManager.primaryIndex
this._updateVisibility();
- },
+ }
- _findMonitorForRect: function(x, y, w, h) {
+ _findMonitorForRect(x, y, w, h) {
// First look at what monitor the center of the rectangle is at
let cx = x + w/2;
let cy = y + h/2;
@@ -926,13 +900,13 @@ Chrome.prototype = {
}
// otherwise on no monitor
return [0, null];
- },
+ }
- _findMonitorForWindow: function(window) {
+ _findMonitorForWindow(window) {
return this._findMonitorForRect(window.x, window.y, window.width, window.height);
- },
+ }
- getMonitorInfoForActor: function(actor) {
+ getMonitorInfoForActor(actor) {
// special case for hideable panel actors:
// due to position and clip they may appear originate on an adjacent monitor
if (actor.maybeGet("_delegate") instanceof Panel.Panel
@@ -943,42 +917,42 @@ Chrome.prototype = {
let [w, h] = actor.get_transformed_size();
let [index, monitor] = this._findMonitorForRect(x, y, w, h);
return [index, monitor];
- },
+ }
// This call guarantees that we return some monitor to simplify usage of it
// In practice all tracked actors should be visible on some monitor anyway
- findMonitorForActor: function(actor) {
+ findMonitorForActor(actor) {
let [index, monitor] = this.getMonitorInfoForActor(actor);
if (monitor)
return monitor;
return this._primaryMonitor; // Not on any monitor, pretend its on the primary
- },
+ }
- findMonitorIndexForActor: function(actor) {
+ findMonitorIndexForActor(actor) {
let [index, monitor] = this.getMonitorInfoForActor(actor);
if (monitor)
return index;
return this._primaryIndex; // Not on any monitor, pretend its on the primary
- },
+ }
- _queueUpdateRegions: function() {
+ _queueUpdateRegions() {
if (!this._updateRegionIdle && !this._freezeUpdateCount)
- this._updateRegionIdle = Mainloop.idle_add(Lang.bind(this, this.updateRegions),
- Meta.PRIORITY_BEFORE_REDRAW);
- },
+ this._updateRegionIdle = Mainloop.idle_add(
+ this.updateRegions.bind(this), Meta.PRIORITY_BEFORE_REDRAW);
+ }
- freezeUpdateRegions: function() {
+ freezeUpdateRegions() {
if (this._updateRegionIdle)
this.updateRegions();
this._freezeUpdateCount++;
- },
+ }
- thawUpdateRegions: function() {
+ thawUpdateRegions() {
this._freezeUpdateCount = --this._freezeUpdateCount >= 0 ? this._freezeUpdateCount : 0;
this._queueUpdateRegions();
- },
+ }
- _windowsRestacked: function() {
+ _windowsRestacked() {
// Figure out where the pointer is in case we lost track of
// it during a grab.
global.sync_pointer();
@@ -990,9 +964,9 @@ Chrome.prototype = {
this._updateVisibility();
else
this._queueUpdateRegions();
- },
+ }
- updateRegions: function() {
+ updateRegions() {
let rects = [], struts = [], i;
if (this._updateRegionIdle) {
diff --git a/js/ui/lightbox.js b/js/ui/lightbox.js
index 9fbbd08f97..79a04e97b5 100644
--- a/js/ui/lightbox.js
+++ b/js/ui/lightbox.js
@@ -110,116 +110,119 @@ var RadialShaderEffect = GObject.registerClass({
* @container and will track any changes in its size. You can override
* this by passing an explicit width and height in @params.
*/
-var Lightbox = class Lightbox {
- constructor(container, params) {
- params = Params.parse(params, { inhibitEvents: false,
- width: null,
- height: null,
- fadeTime: null,
- radialEffect: false,
- });
+var Lightbox = GObject.registerClass(
+class Lightbox extends St.Bin {
+ _init(container, params) {
+ params = Params.parse(params, {
+ inhibitEvents: false,
+ width: null,
+ height: null,
+ fadeTime: null,
+ radialEffect: false,
+ });
+
+ super._init({
+ reactive: params.inhibitEvents,
+ width: params.width,
+ height: params.height,
+ visible: false,
+ });
this._container = container;
this._children = container.get_children();
this._fadeTime = params.fadeTime;
this._radialEffect = Clutter.feature_available(Clutter.FeatureFlags.SHADERS_GLSL) && params.radialEffect;
- this.actor = new St.Bin({ reactive: params.inhibitEvents });
-
if (this._radialEffect)
- this.actor.add_effect(new RadialShaderEffect({ name: 'radial' }));
+ this.add_effect(new RadialShaderEffect({ name: 'radial' }));
else
- this.actor.set({ opacity: 0, style_class: 'lightbox', important: true });
+ this.set({ opacity: 0, style_class: 'lightbox', important: true });
- container.add_actor(this.actor);
- this.actor.raise_top();
- this.actor.hide();
+ container.add_child(this);
+ container.set_child_above_sibling(this, null);
- this.actor.connect('destroy', this._onDestroy.bind(this));
+ this.connect('destroy', this._onDestroy.bind(this));
- if (params.width && params.height) {
- this.actor.width = params.width;
- this.actor.height = params.height;
- } else {
- this.actor.width = container.width;
- this.actor.height = container.height;
- let constraint = new Clutter.BindConstraint({ source: container,
- coordinate: Clutter.BindCoordinate.ALL });
- this.actor.add_constraint(constraint);
+ if (!params.width || !params.height) {
+ this.add_constraint(new Clutter.BindConstraint({
+ source: container,
+ coordinate: Clutter.BindCoordinate.ALL
+ }));
}
- this._actorAddedSignalId = container.connect('actor-added', this._actorAdded.bind(this));
- this._actorRemovedSignalId = container.connect('actor-removed', this._actorRemoved.bind(this));
+ container.connectObject(
+ 'actor-added', this._actorAdded.bind(this),
+ 'actor-removed', this._actorRemoved.bind(this), this);
this._highlighted = null;
}
_actorAdded(container, newChild) {
- let children = this._container.get_children();
- let myIndex = children.indexOf(this.actor);
- let newChildIndex = children.indexOf(newChild);
+ const children = this._container.get_children();
+ const myIndex = children.indexOf(this);
+ const newChildIndex = children.indexOf(newChild);
if (newChildIndex > myIndex) {
// The child was added above the shade (presumably it was
// made the new top-most child). Move it below the shade,
// and add it to this._children as the new topmost actor.
- newChild.lower(this.actor);
+ this._container.set_child_above_sibling(this, newChild);
this._children.push(newChild);
- } else if (newChildIndex == 0) {
+ } else if (newChildIndex === 0) {
// Bottom of stack
this._children.unshift(newChild);
} else {
// Somewhere else; insert it into the correct spot
- let prevChild = this._children.indexOf(children[newChildIndex - 1]);
- if (prevChild != -1) // paranoia
+ const prevChild = this._children.indexOf(children[newChildIndex - 1]);
+ if (prevChild !== -1) // paranoia
this._children.splice(prevChild + 1, 0, newChild);
}
}
- show() {
- this.actor.remove_all_transitions();
+ lightOn() {
+ this.remove_all_transitions();
if (this._radialEffect) {
- this.actor.ease_property(
+ this.ease_property(
'@effects.radial.brightness', VIGNETTE_BRIGHTNESS, {
duration: this._fadeTime / 1000,
mode: Clutter.AnimationMode.EASE_OUT_QUAD
});
- this.actor.ease_property(
+ this.ease_property(
'@effects.radial.sharpness', VIGNETTE_SHARPNESS, {
duration: this._fadeTime / 1000,
mode: Clutter.AnimationMode.EASE_OUT_QUAD
});
} else {
- this.actor.opacity = 0;
- this.actor.ease({
+ this.opacity = 0;
+ this.ease({
opacity: 255,
duration: this._fadeTime / 1000,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
});
}
- this.actor.show();
+ this.show();
}
- hide() {
- this.actor.remove_all_transitions();
+ lightOff() {
+ this.remove_all_transitions();
- let onComplete = () => this.actor.hide();
+ const onComplete = () => this.hide();
if (this._radialEffect) {
- this.actor.ease_property(
+ this.ease_property(
'@effects.radial.brightness', 1.0, {
duration: this._fadeTime / 1000,
mode: Clutter.AnimationMode.EASE_OUT_QUAD
});
- this.actor.ease_property(
+ this.ease_property(
'@effects.radial.sharpness', 0.0, {
duration: this._fadeTime / 1000,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete
});
} else {
- this.actor.ease({
+ this.ease({
opacity: 0,
duration: this._fadeTime / 1000,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
@@ -229,11 +232,11 @@ var Lightbox = class Lightbox {
}
_actorRemoved(container, child) {
- let index = this._children.indexOf(child);
- if (index != -1) // paranoia
+ const index = this._children.indexOf(child);
+ if (index !== -1) // paranoia
this._children.splice(index, 1);
- if (child == this._highlighted)
+ if (child === this._highlighted)
this._highlighted = null;
}
@@ -246,7 +249,7 @@ var Lightbox = class Lightbox {
* argument, all actors will be unhighlighted.
*/
highlight(window) {
- if (this._highlighted == window)
+ if (this._highlighted === window)
return;
// Walk this._children raising and lowering actors as needed.
@@ -255,12 +258,12 @@ var Lightbox = class Lightbox {
// case we may need to indicate some *other* actor as the new
// sibling of the to-be-lowered one.
- let below = this.actor;
+ let below = this;
for (let i = this._children.length - 1; i >= 0; i--) {
- if (this._children[i] == window)
- this._children[i].raise_top();
- else if (this._children[i] == this._highlighted)
- this._children[i].lower(below);
+ if (this._children[i] === window)
+ this._container.set_child_above_sibling(this._children[i], null);
+ else if (this._children[i] === this._highlighted)
+ this._container.set_child_below_sibling(this._children[i], below);
else
below = this._children[i];
}
@@ -268,15 +271,6 @@ var Lightbox = class Lightbox {
this._highlighted = window;
}
- /**
- * destroy:
- *
- * Destroys the lightbox.
- */
- destroy() {
- this.actor.destroy();
- }
-
/**
* _onDestroy:
*
@@ -284,9 +278,6 @@ var Lightbox = class Lightbox {
* by destroying its container or by explicitly calling this.destroy().
*/
_onDestroy() {
- this._container.disconnect(this._actorAddedSignalId);
- this._container.disconnect(this._actorRemovedSignalId);
-
this.highlight(null);
}
-};
+});
diff --git a/js/ui/locatePointer.js b/js/ui/locatePointer.js
index 8201d4b583..89c71fb408 100644
--- a/js/ui/locatePointer.js
+++ b/js/ui/locatePointer.js
@@ -1,14 +1,13 @@
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
-const { Clutter, Gio, GLib, St } = imports.gi;
+const { Clutter, Gio } = imports.gi;
const Ripples = imports.ui.ripples;
-const Lang = imports.lang;
const Main = imports.ui.main;
const LOCATE_POINTER_ENABLED_SCHEMA = "org.cinnamon.desktop.peripherals.mouse"
const LOCATE_POINTER_SCHEMA = "org.cinnamon.muffin"
-var locatePointer = class {
+var LocatePointer = class {
constructor() {
this._enabledSettings = new Gio.Settings({schema_id: LOCATE_POINTER_ENABLED_SCHEMA});
this._enabledSettings.connect('changed::locate-pointer', this._updateKey.bind(this));
@@ -23,7 +22,7 @@ var locatePointer = class {
_updateKey() {
if (this._enabledSettings.get_boolean("locate-pointer")) {
let modifierKeys = this._keySettings.get_strv('locate-pointer-key');
- Main.keybindingManager.addHotKeyArray('locate-pointer', modifierKeys, Lang.bind(this, this.show));
+ Main.keybindingManager.addHotKeyArray('locate-pointer', modifierKeys, this.show.bind(this));
} else {
Main.keybindingManager.removeHotKey('locate-pointer');
}
diff --git a/js/ui/lookingGlass.js b/js/ui/lookingGlass.js
index a5faa22e27..41bd5be8a5 100644
--- a/js/ui/lookingGlass.js
+++ b/js/ui/lookingGlass.js
@@ -265,22 +265,31 @@ function addBorderPaintHook(actor) {
return signalId;
}
-class Inspector {
- constructor() {
- let container = new Cinnamon.GenericContainer({ width: 0,
- height: 0 });
- container.connect('allocate', (...args) => { this._allocate(...args) });
- Main.uiGroup.add_actor(container);
-
- let eventHandler = new St.BoxLayout({ name: 'LookingGlassDialog',
- vertical: true,
- reactive: true });
+var Inspector = GObject.registerClass({
+ Signals: {
+ 'closed': {},
+ 'target': { param_types: [Clutter.Actor.$gtype, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE]},
+ },
+}, class Inspector extends Clutter.Actor {
+ _init() {
+ super._init({
+ width: 0,
+ height: 0,
+ });
+
+ Main.uiGroup.add_actor(this);
+
+ let eventHandler = new St.BoxLayout({
+ name: 'LookingGlassDialog',
+ vertical: true,
+ reactive: true,
+ });
this._eventHandler = eventHandler;
- Main.pushModal(this._eventHandler);
- container.add_actor(eventHandler);
- this._displayText = new St.Label({style: 'text-align: center;'});
+ Main.pushModal(this._eventHandler, undefined, undefined, Cinnamon.ActionMode.LOOKING_GLASS);
+ this.add_child(eventHandler);
+ this._displayText = new St.Label({ style: 'text-align: center;' });
eventHandler.add(this._displayText, { expand: true });
- this._passThroughText = new St.Label({style: 'text-align: center;'});
+ this._passThroughText = new St.Label({ style: 'text-align: center;' });
eventHandler.add(this._passThroughText, { expand: true });
this._borderPaintTarget = null;
@@ -312,11 +321,11 @@ class Inspector {
event.get_key_symbol() === Clutter.KEY_Pause)) {
this.passThroughEvents = !this.passThroughEvents;
this._updatePassthroughText();
- return true;
+ return Clutter.EVENT_STOP;
}
if (this.passThroughEvents)
- return false;
+ return Clutter.EVENT_PROPAGATE;
switch (event.type()) {
case Clutter.EventType.KEY_PRESS:
@@ -328,14 +337,16 @@ class Inspector {
case Clutter.EventType.MOTION:
return this._onMotionEvent(actor, event);
default:
- return true;
+ return Clutter.EVENT_STOP;
}
}
- _allocate(actor, box, flags) {
+ vfunc_allocate(box, flags) {
if (!this._eventHandler)
return;
+ this.set_allocation(box, flags);
+
let primary = Main.layoutManager.primaryMonitor;
let [minWidth, minHeight, natWidth, natHeight] =
@@ -366,7 +377,7 @@ class Inspector {
_onKeyPressEvent(actor, event) {
if (event.get_key_symbol() === Clutter.KEY_Escape)
this._close();
- return true;
+ return Clutter.EVENT_STOP;
}
_onButtonPressEvent(actor, event) {
@@ -375,7 +386,7 @@ class Inspector {
this.emit('target', this._target, stageX, stageY);
}
this._close();
- return true;
+ return Clutter.EVENT_STOP;
}
_onScrollEvent(actor, event) {
@@ -409,12 +420,12 @@ class Inspector {
default:
break;
}
- return true;
+ return Clutter.EVENT_STOP;
}
_onMotionEvent(actor, event) {
this._update(event);
- return true;
+ return Clutter.EVENT_STOP;
}
_update(event) {
@@ -438,9 +449,7 @@ class Inspector {
this._borderPaintId = addBorderPaintHook(this._target);
}
}
-};
-Signals.addSignalMethods(Inspector.prototype);
-
+});
const melangeIFace =
' \
diff --git a/js/ui/main.js b/js/ui/main.js
index b2f7d88613..62bd6dab4a 100644
--- a/js/ui/main.js
+++ b/js/ui/main.js
@@ -117,6 +117,9 @@ const NotificationDaemon = imports.ui.notificationDaemon;
const WindowAttentionHandler = imports.ui.windowAttentionHandler;
const CinnamonDBus = imports.ui.cinnamonDBus;
const Screenshot = imports.ui.screenshot;
+const ScreenShield = imports.ui.screensaver.screenShield;
+const AwayMessageDialog = imports.ui.screensaver.awayMessageDialog;
+const ScreenSaver = imports.misc.screenSaver;
const ThemeManager = imports.ui.themeManager;
const Magnifier = imports.ui.magnifier;
const LocatePointer = imports.ui.locatePointer;
@@ -149,6 +152,10 @@ var slideshowManager = null;
var placesManager = null;
var panelManager = null;
var osdWindowManager = null;
+let _screenShield = null;
+let _screenSaverProxy = null;
+let _screensaverSettings = null;
+var lockdownSettings = null;
var overview = null;
var expo = null;
var runDialog = null;
@@ -191,6 +198,8 @@ var gesturesManager = null;
var keyboardManager = null;
var workspace_names = [];
+var actionMode = Cinnamon.ActionMode.NORMAL;
+
var applet_side = St.Side.TOP; // Kept to maintain compatibility. Doesn't seem to be used anywhere
var deskletContainer = null;
@@ -255,7 +264,9 @@ function _initUserSession() {
systrayManager = new Systray.SystrayManager();
Meta.keybindings_set_custom_handler('panel-run-dialog', function() {
- getRunDialog().open();
+ if (!lockdownSettings.get_boolean('disable-command-line')) {
+ getRunDialog().open();
+ }
});
}
@@ -295,6 +306,12 @@ function start() {
global.logError = _logError;
global.log = _logInfo;
+ try {
+ imports.clearCache;
+ } catch (e) {
+ global.logWarning('CJS clearCache not available. Xlet reloading may not work correctly (cjs update required).');
+ }
+
let cinnamonStartTime = new Date().getTime();
log(`About to start Cinnamon (${Meta.is_wayland_compositor() ? "Wayland" : "X11"} backend)`);
@@ -464,9 +481,11 @@ function start() {
}
magnifier = new Magnifier.Magnifier();
- locatePointer = new LocatePointer.locatePointer();
+ locatePointer = new LocatePointer.LocatePointer();
layoutManager.init();
+ lockdownSettings = new Gio.Settings({ schema_id: 'org.cinnamon.desktop.lockdown' });
+
overview.init();
expo.init();
@@ -519,6 +538,24 @@ function start() {
}
});
+ _screensaverSettings = new Gio.Settings({ schema_id: 'org.cinnamon.desktop.screensaver' });
+
+ // The internal screensaver is the only option for wayland sessions. X11 sessions can use either
+ // the internal one or cinnamon-screensaver (>= 6.7).
+ if (Meta.is_wayland_compositor() || global.settings.get_boolean('internal-screensaver-enabled')) {
+ _screenShield = new ScreenShield.ScreenShield();
+ new ScreenSaver.ScreenSaverService(_screenShield);
+ }
+
+ // Protect security-critical exported functions from being replaced by extensions.
+ for (let fnName of ['lockScreen', 'screenShieldHideKeyboard']) {
+ Object.defineProperty(imports.ui.main, fnName, {
+ value: imports.ui.main[fnName],
+ writable: false,
+ configurable: false
+ });
+ }
+
Promise.all([
AppletManager.init(),
ExtensionSystem.init(),
@@ -1170,6 +1207,42 @@ function getWindowActorsForWorkspace(workspaceIndex) {
});
}
+/**
+ * _shouldFilterKeybinding:
+ * @entry: The keybinding entry from keybindingManager (or undefined)
+ *
+ * Helper function to check if a keybinding should be filtered based on
+ * the current ActionMode. Returns true to BLOCK, false to ALLOW.
+ *
+ * This is used by both _filterKeybinding (window manager path) and
+ * _stageEventHandler (modal/stage capture path).
+ */
+function _shouldFilterKeybinding(entry) {
+ // Check if all keybindings should be blocked
+ if (actionMode == Cinnamon.ActionMode.NONE)
+ return true;
+
+ if (entry === undefined) {
+ // Binding not in our registry, fall back to old behavior
+ return global.stage_input_mode !== Cinnamon.StageInputMode.NORMAL;
+ }
+
+ // Check if current ActionMode is in the allowed modes for this binding
+ // Use bitwise AND - if result is non-zero, the mode is allowed
+ let allowed = (entry.allowedModes & actionMode) !== 0;
+
+ if (allowed) {
+ let lockModes = Cinnamon.ActionMode.LOCK_SCREEN | Cinnamon.ActionMode.UNLOCK_SCREEN;
+ if ((actionMode & lockModes) !== 0 && (entry.allowedModes & lockModes) !== 0) {
+ if (_screenShield && !_screensaverSettings.get_boolean('allow-keyboard-shortcuts')) {
+ return true;
+ }
+ }
+ }
+
+ return !allowed;
+}
+
// This function encapsulates hacks to make certain global keybindings
// work even when we are in one of our modes where global keybindings
// are disabled with a global grab. (When there is a global grab, then
@@ -1191,10 +1264,14 @@ function _stageEventHandler(actor, event) {
let modifierState = Cinnamon.get_event_state(event);
let action = global.display.get_keybinding_action(keyCode, modifierState);
- if (!(event.get_source() instanceof Clutter.Text && (event.get_flags() & Clutter.EventFlags.FLAG_INPUT_METHOD))) {
+ if (!(event.get_source() instanceof Clutter.Text && (event.get_flags() & Clutter.EventFlags.INPUT_METHOD))) {
// This relies on the fact that Clutter.ModifierType is the same as Gdk.ModifierType
if (action > 0) {
- keybindingManager.invoke_keybinding_action_by_id(action);
+ // Check if this keybinding should be filtered based on ActionMode
+ let entry = keybindingManager.getBindingById(action);
+ if (!_shouldFilterKeybinding(entry)) {
+ keybindingManager.invoke_keybinding_action_by_id(action);
+ }
}
}
@@ -1233,7 +1310,9 @@ function _stageEventHandler(actor, event) {
expo.hide();
return true;
case Meta.KeyBindingAction.PANEL_RUN_DIALOG:
- getRunDialog().open();
+ if (!lockdownSettings.get_boolean('disable-command-line')) {
+ getRunDialog().open();
+ }
return true;
}
@@ -1254,6 +1333,7 @@ function _findModal(actor) {
* @timestamp (int): optional timestamp
* @options (Meta.ModalOptions): (optional) flags to indicate that the pointer
* is already grabbed
+ * @mode (Cinnamon.ActionMode): (optional) action mode, defaults to SYSTEM_MODAL
*
* Ensure we are in a mode where all keyboard and mouse input goes to
* the stage, and focus @actor. Multiple calls to this function act in
@@ -1268,12 +1348,18 @@ function _findModal(actor) {
* initiated event. If not provided then the value of
* global.get_current_time() is assumed.
*
+ * @mode determines which keybindings and actions are allowed while modal.
+ * If not provided, defaults to SYSTEM_MODAL.
+ *
* Returns (boolean): true iff we successfully acquired a grab or already had one
*/
-function pushModal(actor, timestamp, options) {
+function pushModal(actor, timestamp, options, mode) {
if (timestamp == undefined)
timestamp = global.get_current_time();
+ if (mode == undefined)
+ mode = Cinnamon.ActionMode.SYSTEM_MODAL;
+
if (modalCount == 0) {
if (!global.begin_modal(timestamp, options ? options : 0)) {
log('pushModal: invocation of begin_modal failed');
@@ -1284,6 +1370,8 @@ function pushModal(actor, timestamp, options) {
global.set_stage_input_mode(Cinnamon.StageInputMode.FULLSCREEN);
+ actionMode = mode;
+
modalCount += 1;
let actorDestroyId = actor.connect('destroy', function() {
let index = _findModal(actor);
@@ -1294,7 +1382,8 @@ function pushModal(actor, timestamp, options) {
let record = {
actor: actor,
focus: global.stage.get_key_focus(),
- destroyId: actorDestroyId
+ destroyId: actorDestroyId,
+ actionMode: mode
};
if (record.focus != null) {
record.focusDestroyId = record.focus.connect('destroy', function() {
@@ -1310,6 +1399,29 @@ function pushModal(actor, timestamp, options) {
return true;
}
+/**
+ * setActionMode:
+ * @actor (Clutter.Actor): actor currently holding the modal grab.
+ * @mode (Cinnamon.ActionMode): the new action mode.
+ *
+ * Change the action mode for an existing modal grab without releasing
+ * and reacquiring the grab. This avoids a window where there is no
+ * grab, which is important for the lock screen.
+ */
+function setActionMode(actor, mode) {
+ let focusIndex = _findModal(actor);
+ if (focusIndex < 0) {
+ global.logWarning('setActionMode: actor is not in the modal stack');
+ return;
+ }
+
+ if (modalActorFocusStack[focusIndex].actionMode === mode)
+ return;
+
+ actionMode = mode;
+ modalActorFocusStack[focusIndex].actionMode = mode;
+}
+
/**
* popModal:
* @actor (Clutter.Actor): actor passed to original invocation of pushModal().
@@ -1360,11 +1472,15 @@ function popModal(actor, timestamp) {
}
modalActorFocusStack.splice(focusIndex, 1);
- if (modalCount > 0)
+ if (modalCount > 0) {
+ let topModal = modalActorFocusStack[modalActorFocusStack.length - 1];
+ actionMode = topModal.actionMode;
return;
+ }
global.end_modal(timestamp);
global.set_stage_input_mode(Cinnamon.StageInputMode.NORMAL);
+ actionMode = Cinnamon.ActionMode.NORMAL;
layoutManager.updateChrome(true);
@@ -1654,3 +1770,36 @@ function closeEndSessionDialog() {
endSessionDialog.close();
endSessionDialog = null;
}
+
+function lockScreen(askForAwayMessage) {
+ if (lockdownSettings.get_boolean('disable-lock-screen')) {
+ return;
+ }
+
+ if (askForAwayMessage && _screensaverSettings.get_boolean('ask-for-away-message')) {
+ let dialog = new AwayMessageDialog.AwayMessageDialog((message) => {
+ _doLock(message);
+ });
+ dialog.open();
+ return;
+ }
+
+ _doLock(null);
+}
+
+function _doLock(awayMessage) {
+ if (_screenShield) {
+ _screenShield.lock(false, awayMessage);
+ return;
+ }
+
+ if (_screenSaverProxy === null) {
+ _screenSaverProxy = new ScreenSaver.ScreenSaverProxy();
+ }
+
+ _screenSaverProxy.LockRemote(awayMessage || "");
+}
+
+function screenShieldHideKeyboard() {
+ _screenShield?._hideScreensaverKeyboard();
+}
diff --git a/js/ui/messageTray.js b/js/ui/messageTray.js
index 13ddb612b7..00ac0d67fc 100644
--- a/js/ui/messageTray.js
+++ b/js/ui/messageTray.js
@@ -149,7 +149,7 @@ URLHighlighter.prototype = {
let urlId = this._findUrlAtPos(event);
if (urlId != -1 && !this._cursorChanged) {
- global.set_cursor(Cinnamon.Cursor.POINTING_HAND);
+ global.set_cursor(Cinnamon.Cursor.POINTER);
this._cursorChanged = true;
} else if (urlId == -1) {
global.unset_cursor();
diff --git a/js/ui/modalDialog.js b/js/ui/modalDialog.js
index 74fac68f1e..6d34a4761d 100644
--- a/js/ui/modalDialog.js
+++ b/js/ui/modalDialog.js
@@ -197,7 +197,7 @@ var ModalDialog = GObject.registerClass({
this.dialogLayout.opacity = 255;
if (this._lightbox)
- this._lightbox.show();
+ this._lightbox.lightOn();
this.opacity = 0;
this.show();
this.ease({
@@ -305,14 +305,15 @@ var ModalDialog = GObject.registerClass({
* pushModal:
* @timestamp (int): (optional) timestamp optionally used to associate the
* call with a specific user initiated event
+ * @mode (Cinnamon.ActionMode): (optional) action mode, defaults to SYSTEM_MODAL
*
* Pushes the modal to the modal stack so that it grabs the required
* inputs.
*/
- pushModal(timestamp) {
+ pushModal(timestamp, mode) {
if (this._hasModal)
return true;
- if (!Main.pushModal(this, timestamp))
+ if (!Main.pushModal(this, timestamp, undefined, mode))
return false;
this._hasModal = true;
diff --git a/js/ui/overview.js b/js/ui/overview.js
index e2d74b1417..0406a0f609 100644
--- a/js/ui/overview.js
+++ b/js/ui/overview.js
@@ -228,7 +228,7 @@ Overview.prototype = {
if (this._shown)
return;
// Do this manually instead of using _syncInputMode, to handle failure
- if (!Main.pushModal(this._group))
+ if (!Main.pushModal(this._group, undefined, undefined, Cinnamon.ActionMode.OVERVIEW))
return;
this._modal = true;
this._shown = true;
@@ -363,7 +363,7 @@ Overview.prototype = {
if (this._shown) {
if (!this._modal) {
- if (Main.pushModal(this._group))
+ if (Main.pushModal(this._group, undefined, undefined, Cinnamon.ActionMode.OVERVIEW))
this._modal = true;
else
this.hide();
diff --git a/js/ui/panel.js b/js/ui/panel.js
index 73b09d2b50..a2628a7ed6 100644
--- a/js/ui/panel.js
+++ b/js/ui/panel.js
@@ -77,14 +77,6 @@ const Direction = {
RIGHT : 1
};
-const CornerType = {
- topleft : 0,
- topright : 1,
- bottomleft : 2,
- bottomright : 3,
- dummy : 4
-};
-
var PanelLoc = {
top : 0,
bottom : 1,
@@ -98,49 +90,6 @@ const PanelDefElement = {
POSITION: 2
};
-// To make sure the panel corners blend nicely with the panel,
-// we draw background and borders the same way, e.g. drawing
-// them as filled shapes from the outside inwards instead of
-// using cairo stroke(). So in order to give the border the
-// appearance of being drawn on top of the background, we need
-// to blend border and background color together.
-// For that purpose we use the following helper methods, taken
-// from st-theme-node-drawing.c
-function _norm(x) {
- return Math.round(x / 255);
-}
-
-function _over(srcColor, dstColor) {
- let src = _premultiply(srcColor);
- let dst = _premultiply(dstColor);
- let result = new Clutter.Color();
-
- result.alpha = src.alpha + _norm((255 - src.alpha) * dst.alpha);
- result.red = src.red + _norm((255 - src.alpha) * dst.red);
- result.green = src.green + _norm((255 - src.alpha) * dst.green);
- result.blue = src.blue + _norm((255 - src.alpha) * dst.blue);
-
- return _unpremultiply(result);
-}
-
-function _premultiply(color) {
- return new Clutter.Color({ red: _norm(color.red * color.alpha),
- green: _norm(color.green * color.alpha),
- blue: _norm(color.blue * color.alpha),
- alpha: color.alpha });
-};
-
-function _unpremultiply(color) {
- if (color.alpha == 0)
- return new Clutter.Color();
-
- let red = Math.min((color.red * 255 + 127) / color.alpha, 255);
- let green = Math.min((color.green * 255 + 127) / color.alpha, 255);
- let blue = Math.min((color.blue * 255 + 127) / color.alpha, 255);
- return new Clutter.Color({ red: red, green: green,
- blue: blue, alpha: color.alpha });
-};
-
/**
* checkPanelUpgrade:
*
@@ -426,9 +375,6 @@ PanelManager.prototype = {
let stash = []; // panel id, monitor, panel type
let monitorCount = global.display.get_n_monitors();
- let panels_used = []; // [monitor] [top, bottom, left, right]. Used to keep track of which panel types are in use,
- // as we need knowledge of the combinations in order to instruct the correct panel to create a corner
-
let panel_defs = getPanelsEnabledList();
//
// First pass through just to count the monitors, as there is no ordering to rely on
@@ -478,47 +424,23 @@ PanelManager.prototype = {
setPanelsEnabledList(clean_defs);
}
- //
- // initialise the array that records which panels are used (so combinations can be used to select corners)
- //
- for (let i = 0; i <= monitorCount; i++) {
- panels_used.push([]);
- panels_used[i][0] = false;
- panels_used[i][1] = false;
- panels_used[i][2] = false;
- panels_used[i][3] = false;
- }
- //
// set up the list of panels
- //
for (let i = 0, len = good_defs.length; i < len; i++) {
let elements = good_defs[i].split(":");
let jj = getPanelLocFromName(elements[PanelDefElement.POSITION]); // panel orientation
monitor = parseInt(elements[PanelDefElement.MONITOR]);
- panels_used[monitor][jj] = true;
stash[i] = [parseInt(elements[PanelDefElement.ID]), monitor, jj]; // load what we are going to use to call loadPanel into an array
}
- //
// When using mixed horizontal and vertical panels draw the vertical panels first.
// This is done so that when using a box shadow on the panel to create a border the border will be drawn over the
// top of the vertical panel.
- //
- // Draw corners where necessary. NB no corners necessary where there is no panel for a full screen window to butt up against.
- // logic for loading up panels in the right order and drawing corners relies on ordering by monitor
- // Corners will go on the left and right panels if there are any, else on the top and bottom
- // corner drawing parameters passed are left, right for horizontals, top, bottom for verticals.
- //
- // panel corners are optional and not used in many themes. However there is no measurable gain in trying to suppress them
- // if the theme does not have them
-
for (let i = 0; i <= monitorCount; i++) {
let pleft, pright;
for (let j = 0, len = stash.length; j < len; j++) {
- let drawcorner = [false,false];
if (stash[j][2] == PanelLoc.left && stash[j][1] == i) {
pleft = this._loadPanel(stash[j][0], stash[j][1], stash[j][2], [true,true]);
}
@@ -526,14 +448,10 @@ PanelManager.prototype = {
pright = this._loadPanel(stash[j][0], stash[j][1], stash[j][2], [true,true]);
}
if (stash[j][2] == PanelLoc.bottom && stash[j][1] == i) {
- drawcorner[0] = !(panels_used[i][2]);
- drawcorner[1] = !(panels_used[i][3]);
- this._loadPanel(stash[j][0], stash[j][1], stash[j][2], drawcorner);
+ this._loadPanel(stash[j][0], stash[j][1], stash[j][2]);
}
if (stash[j][2] == PanelLoc.top && stash[j][1] == i) {
- drawcorner[0] = !(panels_used[i][2]);
- drawcorner[1] = !(panels_used[i][3]);
- this._loadPanel(stash[j][0], stash[j][1], stash[j][2], drawcorner);
+ this._loadPanel(stash[j][0], stash[j][1], stash[j][2]);
}
}
//
@@ -834,7 +752,6 @@ PanelManager.prototype = {
* @ID (integer): panel id
* @monitorIndex (integer): index of monitor of panel
* @panelPosition (integer): where the panel should be
- * @drawcorner (array): whether to draw corners for [left, right]
* @panelList (array): (optional) the list in which the new panel should be appended to (not necessarily this.panels, c.f. _onPanelsEnabledChanged) Default: this.panels
* @metaList(array): (optional) the list in which the new panel metadata should be appended to (not necessarily this.panelsMeta, c.f. _onPanelsEnabledChanged)
* Default: this.panelsMeta
@@ -843,7 +760,7 @@ PanelManager.prototype = {
*
* Returns (Panel.Panel): Panel created
*/
- _loadPanel: function(ID, monitorIndex, panelPosition, drawcorner, panelList, metaList) {
+ _loadPanel: function(ID, monitorIndex, panelPosition, panelList, metaList) {
if (!panelList) panelList = this.panels;
if (!metaList) metaList = this.panelsMeta;
@@ -893,7 +810,7 @@ PanelManager.prototype = {
return null;
}
let[toppheight,botpheight] = heightsUsedMonitor(monitorIndex, panelList);
- panelList[ID] = new Panel(ID, monitorIndex, panelPosition, toppheight, botpheight, drawcorner); // create a new panel
+ panelList[ID] = new Panel(ID, monitorIndex, panelPosition, toppheight, botpheight);
this.panelCount += 1;
return panelList[ID];
@@ -926,8 +843,6 @@ PanelManager.prototype = {
let newPanels = new Array(this.panels.length);
let newMeta = new Array(this.panels.length);
- let drawcorner = [false,false];
-
let panelProperties = getPanelsEnabledList();
@@ -966,7 +881,6 @@ PanelManager.prototype = {
let panel = this._loadPanel(ID,
mon,
ploc,
- drawcorner,
newPanels,
newMeta);
if (panel)
@@ -989,23 +903,16 @@ PanelManager.prototype = {
this.panels = newPanels;
this.panelsMeta = newMeta;
- //
+
// Adjust any vertical panel heights so as to fit snugly between horizontal panels
// Scope for minor optimisation here, doesn't need to adjust verticals if no horizontals added or removed
// or if any change from making space for panel dummys needs to be reflected.
- //
- // Draw any corners that are necessary. Note that updatePosition will have stripped off corners
- // from moved panels, and the new panel is created without corners. However unchanged panels may have corners
- // that might not be wanted now. Easiest thing is to strip every existing corner off and re-add
- //
for (let i = 0, len = this.panels.length; i < len; i++) {
if (this.panels[i]) {
if (this.panels[i].panelPosition == PanelLoc.left || this.panels[i].panelPosition == PanelLoc.right)
this.panels[i]._moveResizePanel();
- this.panels[i]._destroycorners();
}
}
- this._fullCornerLoad(panelProperties);
this._setMainPanel();
this._checkCanAdd();
@@ -1022,101 +929,9 @@ PanelManager.prototype = {
this.handling_panels_changed = false;
},
- /**
- * _fullCornerLoad :
- * @panelProperties : panels-enabled settings string
- *
- * Load all corners
- */
- _fullCornerLoad: function(panelProperties) {
- let monitor = 0;
- let monitorCount = -1;
- let panels_used = []; // [monitor] [top, bottom, left, right]. Used to keep track of which panel types are in use,
- // as we need knowledge of the combinations in order to instruct the correct panel to create a corner
- let stash = []; // panel id, monitor, panel type
-
- //
- // First pass through just to count the monitors, as there is no ordering to rely on
- //
- for (let i = 0, len = panelProperties.length; i < len; i++) {
- let elements = panelProperties[i].split(":");
- if (elements.length != 3) {
- global.log("Invalid panel definition: " + panelProperties[i]);
- continue;
- }
-
- monitor = parseInt(elements[1]);
- if (monitor > monitorCount)
- monitorCount = monitor;
- }
- //
- // initialise the array that records which panels are used (so combinations can be used to select corners)
- //
- for (let i = 0; i <= monitorCount; i++) {
- panels_used.push([]);
- panels_used[i][0] = false;
- panels_used[i][1] = false;
- panels_used[i][2] = false;
- panels_used[i][3] = false;
- }
- //
- // set up the list of panels
- //
- for (let i = 0, len = panelProperties.length; i < len; i++) {
- let elements = panelProperties[i].split(":");
- if (elements.length != 3) {
- global.log("Invalid panel definition: " + panelProperties[i]);
- continue;
- }
- let monitor = parseInt(elements[1]);
- let jj = getPanelLocFromName(elements[2]);
- panels_used[monitor][jj] = true;
-
- stash[i] = [parseInt(elements[0]),monitor,jj];
- }
-
- // draw corners on each monitor in turn. Note that the panel.drawcorner
- // variable needs to be set so the allocation code runs as desired
-
- for (let i = 0; i <= monitorCount; i++) {
- for (let j = 0, len = stash.length; j < len; j++) {
- let drawcorner = [false, false];
- if (stash[j][2] == PanelLoc.bottom && stash[j][1] == i) {
- drawcorner[0] = !(panels_used[i][2]);
- drawcorner[1] = !(panels_used[i][3]);
- if (this.panels[stash[j][0]]) { // panel will not have loaded if previous monitor disconnected etc.
- this.panels[stash[j][0]].drawcorner = drawcorner;
- this.panels[stash[j][0]].drawCorners(drawcorner);
- }
- }
- if (stash[j][2] == PanelLoc.left && stash[j][1] == i) {
- if (this.panels[stash[j][0]]) {
- this.panels[stash[j][0]].drawcorner = [true,true];
- this.panels[stash[j][0]].drawCorners([true,true]);
- }
- }
- if (stash[j][2] == PanelLoc.right && stash[j][1] == i) {
- if (this.panels[stash[j][0]]) {
- this.panels[stash[j][0]].drawcorner = [true,true];
- this.panels[stash[j][0]].drawCorners([true,true]);
- }
- }
- if (stash[j][2] == PanelLoc.top && stash[j][1] == i) {
- drawcorner[0] = !(panels_used[i][2]);
- drawcorner[1] = !(panels_used[i][3]);
- if (this.panels[stash[j][0]]) {
- this.panels[stash[j][0]].drawcorner = drawcorner;
- this.panels[stash[j][0]].drawCorners(drawcorner);
- }
- }
- }
- }
- },
-
_onMonitorsChanged: function() {
const oldCount = this.monitorCount;
this.monitorCount = global.display.get_n_monitors();
- let drawcorner = [false, false];
let panelProperties = getPanelsEnabledList()
// adjust any changes to logical/xinerama monitor relationships
@@ -1131,7 +946,7 @@ PanelManager.prototype = {
// - the monitor may just have been reconnected
if (this.panelsMeta[i][0] < this.monitorCount) // just check that the monitor is there
{
- let panel = this._loadPanel(i, this.panelsMeta[i][0], this.panelsMeta[i][1], drawcorner);
+ let panel = this._loadPanel(i, this.panelsMeta[i][0], this.panelsMeta[i][1]);
if (panel)
AppletManager.loadAppletsOnPanel(panel);
}
@@ -1160,14 +975,6 @@ PanelManager.prototype = {
this._showDummyPanels(this.dummyCallback);
}
- // clear corners, then re add them
- for (let i = 0, len = this.panels.length; i < len; i++) {
- if (this.panels[i])
- this.panels[i]._destroycorners();
- }
-
- this._fullCornerLoad(panelProperties);
-
this._setMainPanel();
this._checkCanAdd();
this._updateAllPointerBarriers();
@@ -1481,202 +1288,6 @@ TextShadower.prototype = {
}
}
};
- /**
- * PanelCorner:
- * @box: the box in a panel the corner is associated with
- * @side: the side of the box a text or icon/text applet starts from (RTL or LTR driven)
- * @cornertype: top left, bottom right etc.
- *
- * Sets up a panel corner
- *
- * The panel corners are there for a non-obvious reason. They are used as the positioning points for small
- * drawing areas that use some optional css to draw small filled arcs (in the repaint function). This allows
- * windows with rounded corners to be blended into the panels in some distros, gnome shell in particular.
- * In mint tiling and full screen removes any rounded window corners anyway, so this optional css is not there in
- * the main mint themes, and the corner/cairo functionality is unused in this case. Where the corners are used they will be
- * positioned so as to fill in the tiny gap at the corners of full screen windows, and if themed right they
- * will be invisible to the user, other than the window will appear to go right up to the corner when full screen
- */
-function PanelCorner(box, side, cornertype) {
- this._init(box, side, cornertype);
-}
-
-PanelCorner.prototype = {
- _init: function(box, side, cornertype) {
- this._side = side;
- this._box = box;
- this._cornertype = cornertype;
- this.cornerRadius = 0;
-
- this.actor = new St.DrawingArea({ style_class: 'panel-corner' });
-
- this.actor.connect('style-changed', Lang.bind(this, this._styleChanged));
- this.actor.connect('repaint', Lang.bind(this, this._repaint));
- },
-
- _repaint: function() {
- //
- // This is all about painting corners just outside the panels so as to create a seamless visual impression for full screen windows
- // with curved corners that butt up against a panel.
- // So ... top left corner wants to be at the bottom left of the top panel. top right wants to be in the corresponding place on the right
- // Bottom left corner wants to be at the top left of the bottom panel. bottom right in the corresponding place on the right.
- // No panel, no corner necessary.
- // If there are vertical panels as well then we want to shift these in by the panel width so if there are vertical panels but no horizontal
- // then the corners are top right and left to right of left panel, and same to left of right panel
- //
- if (this._cornertype == CornerType.dummy) return;
-
- let node = this.actor.get_theme_node();
-
- if (node) {
- let xOffsetDirection = 0;
- let yOffsetDirection = 0;
-
- let cornerRadius = node.get_length("-panel-corner-radius");
- let innerBorderWidth = node.get_length('-panel-corner-inner-border-width');
- let outerBorderWidth = node.get_length('-panel-corner-outer-border-width');
-
- let backgroundColor = node.get_color('-panel-corner-background-color');
- let innerBorderColor = node.get_color('-panel-corner-inner-border-color');
- let outerBorderColor = node.get_color('-panel-corner-outer-border-color');
-
- // Save suitable offset directions for later use
-
- xOffsetDirection = (this._cornertype == CornerType.topleft || this._cornertype == CornerType.bottomleft)
- ? -1 : 1;
-
- yOffsetDirection = (this._cornertype == CornerType.topleft || this._cornertype == CornerType.topright)
- ? -1 : 1;
-
- let cr = this.actor.get_context();
- cr.setOperator(Cairo.Operator.SOURCE);
- cr.save();
-
- // Draw arc, lines and fill to create a concave triangle
-
- if (this._cornertype == CornerType.topleft) {
- cr.moveTo(0, 0);
- cr.arc( cornerRadius,
- innerBorderWidth + cornerRadius,
- cornerRadius,
- Math.PI,
- 3 * Math.PI / 2); //xc, yc, radius, angle from, angle to. NB note small offset in y direction
- cr.lineTo(cornerRadius, 0);
- } else if (this._cornertype == CornerType.topright) {
- cr.moveTo(0, 0);
- cr.arc( 0,
- innerBorderWidth + cornerRadius,
- cornerRadius,
- 3 * Math.PI / 2,
- 2 * Math.PI);
- cr.lineTo(cornerRadius, 0);
- } else if (this._cornertype == CornerType.bottomleft) {
- cr.moveTo(0, cornerRadius);
- cr.lineTo(cornerRadius,cornerRadius);
- cr.lineTo(cornerRadius, cornerRadius-innerBorderWidth);
- cr.arc( cornerRadius,
- -innerBorderWidth,
- cornerRadius,
- Math.PI/2,
- Math.PI);
- cr.lineTo(0,cornerRadius);
- } else if (this._cornertype == CornerType.bottomright) {
- cr.moveTo(0,cornerRadius);
- cr.lineTo(cornerRadius, cornerRadius);
- cr.lineTo(cornerRadius, 0);
- cr.arc( 0,
- -innerBorderWidth,
- cornerRadius,
- 0,
- Math.PI/2);
- cr.lineTo(0, cornerRadius);
- }
-
- cr.closePath();
-
- let savedPath = cr.copyPath(); // save basic shape for reuse
-
- let over = _over(innerBorderColor,
- _over(outerBorderColor, backgroundColor)); // colour inner over outer over background.
- Clutter.cairo_set_source_color(cr, over);
- cr.fill();
-
- over = _over(innerBorderColor, backgroundColor); //colour inner over background
- Clutter.cairo_set_source_color(cr, over);
-
- // Draw basic shape with vertex shifted diagonally outwards by the border width
-
- let offset = outerBorderWidth;
- cr.translate(xOffsetDirection * offset, yOffsetDirection * offset); // move by x,y
- cr.appendPath(savedPath);
- cr.fill();
-
- // Draw a small rectangle over the end of the arc on the inwards side
- // why ? pre-existing code, reason for creating this squared off end to the shape is not clear.
-
- if (this._cornertype == CornerType.topleft)
- cr.rectangle(cornerRadius - offset,
- 0,
- offset,
- outerBorderWidth); // x,y,width,height
- else if (this._cornertype == CornerType.topright)
- cr.rectangle(0,
- 0,
- offset,
- outerBorderWidth);
- else if (this._cornertype == CornerType.bottomleft)
- cr.rectangle(cornerRadius - offset,
- cornerRadius - offset,
- offset,
- outerBorderWidth);
- else if (this._cornertype.bottomright)
- cr.rectangle(0,
- cornerRadius - offset,
- offset,
- outerBorderWidth);
- cr.fill();
- offset = innerBorderWidth;
- Clutter.cairo_set_source_color(cr, backgroundColor); // colour background
-
- // Draw basic shape with vertex shifted diagonally outwards by the border width, in background colour
-
- cr.translate(xOffsetDirection * offset, yOffsetDirection * offset);
- cr.appendPath(savedPath);
- cr.fill();
- cr.restore();
-
- cr.$dispose();
-
- // Trim things down to a neat and tidy box
-
- this.actor.set_clip(0,0,cornerRadius,cornerRadius);
- }
- },
-
- _styleChanged: function() {
- let node = this.actor.get_theme_node();
-
- let cornerRadius = node.get_length("-panel-corner-radius");
- let innerBorderWidth = node.get_length('-panel-corner-inner-border-width');
-
- this.actor.set_size(cornerRadius, cornerRadius);
- this.actor.set_anchor_point(0, 0);
-
- // since the corners are a child actor of the panel, we need to account
- // for their size when setting the panel clip region. we keep track here
- // so the panel can easily check it.
- this.cornerRadius = cornerRadius;
-
- if (this._box.is_finalized()) return;
- // ugly hack: force the panel to reset its clip region since we just added
- // to the total allocation after it has already clipped to its own
- // allocation
- let panel = this._box.get_parent();
- // for some reason style-changed is called on destroy
- if (panel && panel._delegate)
- panel._delegate._setClipRegion(panel._delegate._hidden);
- }
-}; // end of panel corner
function SettingsLauncher(label, keyword, icon) {
this._init(label, keyword, icon);
@@ -2059,7 +1670,6 @@ PanelZoneDNDHandler.prototype = {
* @monitorIndex (int): the index of the monitor containing the panel
* @toppanelHeight (int): the height already taken on the screen by a top panel
* @bottompanelHeight (int): the height already taken on the screen by a bottom panel
- * @drawcorner (array): [left, right] whether to draw corners alongside the panel
*
* @monitor (Meta.Rectangle): the geometry (bounding box) of the monitor
* @panelPosition (integer): where the panel is on the screen
@@ -2075,15 +1685,14 @@ PanelZoneDNDHandler.prototype = {
*
* This represents a panel on the screen.
*/
-function Panel(id, monitorIndex, panelPosition, toppanelHeight, bottompanelHeight, drawcorner) {
- this._init(id, monitorIndex, panelPosition, toppanelHeight, bottompanelHeight, drawcorner);
+function Panel(id, monitorIndex, panelPosition, toppanelHeight, bottompanelHeight) {
+ this._init(id, monitorIndex, panelPosition, toppanelHeight, bottompanelHeight);
}
Panel.prototype = {
- _init : function(id, monitorIndex, panelPosition, toppanelHeight, bottompanelHeight, drawcorner) {
+ _init : function(id, monitorIndex, panelPosition, toppanelHeight, bottompanelHeight) {
this.panelId = id;
- this.drawcorner = drawcorner;
this.monitorIndex = monitorIndex;
this.monitor = global.display.get_monitor_geometry(monitorIndex);
this.panelPosition = panelPosition;
@@ -2142,8 +1751,6 @@ Panel.prototype = {
this._centerBoxDNDHandler = new PanelZoneDNDHandler(this._centerBox, 'center', this.panelId);
this._rightBoxDNDHandler = new PanelZoneDNDHandler(this._rightBox, 'right', this.panelId);
- this.drawCorners(drawcorner);
-
this.addContextMenuToPanel(this.panelPosition);
Main.layoutManager.addChrome(this.actor, { addToWindowgroup: false });
@@ -2171,88 +1778,6 @@ Panel.prototype = {
this._onPanelZoneSizesChanged();
},
- drawCorners: function(drawcorner)
- {
-
- if (this.panelPosition == PanelLoc.top || this.panelPosition == PanelLoc.bottom) { // horizontal panels
- if (drawcorner[0]) { // left corner
- if (this.panelPosition == PanelLoc.top) {
- if (this.actor.get_direction() == St.TextDirection.RTL) // right to left text direction e.g. arabic
- this._leftCorner = new PanelCorner(this._rightBox, St.Side.LEFT, CornerType.topleft);
- else // left to right text direction
- this._leftCorner = new PanelCorner(this._leftBox, St.Side.LEFT, CornerType.topleft);
- } else { // bottom panel
- if (this.actor.get_direction() == St.TextDirection.RTL) // right to left text direction e.g. arabic
- this._leftCorner = new PanelCorner(this._rightBox, St.Side.LEFT, CornerType.bottomleft);
- else // left to right text direction
- this._leftCorner = new PanelCorner(this._leftBox, St.Side.LEFT, CornerType.bottomleft);
- }
- }
- if (drawcorner[1]) { // right corner
- if (this.panelPosition == PanelLoc.top) {
- if (this.actor.get_direction() == St.TextDirection.RTL) // right to left text direction e.g. arabic
- this._rightCorner = new PanelCorner(this._leftBox, St.Side.RIGHT,CornerType.topright);
- else // left to right text direction
- this._rightCorner = new PanelCorner(this._rightBox, St.Side.RIGHT,CornerType.topright);
- } else { // bottom
- if (this.actor.get_direction() == St.TextDirection.RTL) // right to left text direction e.g. arabic
- this._rightCorner = new PanelCorner(this._leftBox, St.Side.RIGHT,CornerType.bottomright);
- else // left to right text direction
- this._rightCorner = new PanelCorner(this._rightBox, St.Side.RIGHT,CornerType.bottomright);
- }
- }
- } else { // vertical panels
- if (this.panelPosition == PanelLoc.left) { // left panel
- if (drawcorner[0]) {
- if (this.actor.get_direction() == St.TextDirection.RTL) // right to left text direction
- this._leftCorner = new PanelCorner(this._rightBox, St.Side.TOP, CornerType.topleft);
- else
- this._leftCorner = new PanelCorner(this._leftBox, St.Side.TOP, CornerType.topleft);
- }
- if (drawcorner[1])
- {
- if (this.actor.get_direction() == St.TextDirection.RTL) // right to left text direction
- this._rightCorner = new PanelCorner(this._leftBox, St.Side.BOTTOM, CornerType.bottomleft);
- else
- this._rightCorner = new PanelCorner(this._rightBox, St.Side.BOTTOM, CornerType.bottomleft);
- }
- } else { // right panel
- if (drawcorner[0]) {
- if (this.actor.get_direction() == St.TextDirection.RTL) // right to left text direction
- this._leftCorner = new PanelCorner(this._rightBox, St.Side.TOP, CornerType.topright);
- else
- this._leftCorner = new PanelCorner(this._leftBox, St.Side.TOP, CornerType.topright);
- }
- if (drawcorner[1]) {
- if (this.actor.get_direction() == St.TextDirection.RTL) // right to left text direction;
- this._rightCorner = new PanelCorner(this._leftBox, St.Side.BOTTOM, CornerType.bottomright);
- else
- this._rightCorner = new PanelCorner(this._rightBox, St.Side.BOTTOM, CornerType.bottomright);
- }
- }
- }
-
- if (this.actor.is_finalized()) return;
-
- if (this._leftCorner)
- this.actor.add_actor(this._leftCorner.actor);
- if (this._rightCorner)
- this.actor.add_actor(this._rightCorner.actor);
- },
-
- _destroycorners: function()
- {
- if (this._leftCorner) {
- this._leftCorner.actor.destroy();
- this._leftCorner = null;
- }
- if (this._rightCorner) {
- this._rightCorner.actor.destroy();
- this._rightCorner = null;
- }
- this.drawcorner = [false,false];
- },
-
/**
* updatePosition:
* @monitorIndex: integer, index of monitor
@@ -2266,11 +1791,6 @@ Panel.prototype = {
this._positionChanged = true;
this.monitor = global.display.get_monitor_geometry(monitorIndex);
- //
- // If there are any corners then remove them - they may or may not be required
- // in the new position, so we cannot just move them
- //
- this._destroycorners();
this._set_orientation();
@@ -2373,7 +1893,6 @@ Panel.prototype = {
this._leftBox.destroy();
this._centerBox.destroy();
this._rightBox.destroy();
- this._destroycorners();
this._signalManager.disconnectAllSignals()
@@ -2792,15 +2311,6 @@ Panel.prototype = {
let isHorizontal = this.panelPosition == PanelLoc.top
|| this.panelPosition == PanelLoc.bottom;
- // determine corners size so we can extend allocation when not
- // hiding or animating.
- let cornerRadius = 0;
- if (this._leftCorner && this._leftCorner.cornerRadius > 0) {
- cornerRadius = this._leftCorner.cornerRadius;
- } else if (this._rightCorner && this._rightCorner.cornerRadius > 0) {
- cornerRadius = this._rightCorner.cornerRadius;
- }
-
// determine exposed amount of panel
let exposedAmount;
if (isHorizontal) {
@@ -2821,8 +2331,8 @@ Panel.prototype = {
// determine offset & set clip
// top/left panels: must offset by the hidden amount
- // bottom/right panels: if showing must offset by shadow size and corner radius
- // all panels: if showing increase exposedAmount by shadow size and corner radius
+ // bottom/right panels: if showing must offset by shadow size
+ // all panels: if showing increase exposedAmount by shadow size
// we use only the shadowbox x1 or y1 (offset) to determine shadow size
// as some themes use an offset shadow to draw only on one side whereas
@@ -2834,10 +2344,10 @@ Panel.prototype = {
clipOffsetY = this.actor.height - exposedAmount;
} else {
if (!hidden)
- clipOffsetY = this._shadowBox.y1 - cornerRadius;
+ clipOffsetY = this._shadowBox.y1;
}
if (!hidden)
- exposedAmount += Math.abs(this._shadowBox.y1) + cornerRadius;
+ exposedAmount += Math.abs(this._shadowBox.y1);
this.actor.set_clip(0, clipOffsetY, this.actor.width, exposedAmount);
} else {
let clipOffsetX = 0;
@@ -2845,10 +2355,10 @@ Panel.prototype = {
clipOffsetX = this.actor.width - exposedAmount;
} else {
if (!hidden)
- clipOffsetX = this._shadowBox.x1 - cornerRadius;
+ clipOffsetX = this._shadowBox.x1;
}
if (!hidden)
- exposedAmount += Math.abs(this._shadowBox.x1) + cornerRadius;
+ exposedAmount += Math.abs(this._shadowBox.x1);
this.actor.set_clip(clipOffsetX, 0, exposedAmount, this.actor.height);
}
// Force the layout manager to update the input region
@@ -3524,14 +3034,6 @@ Panel.prototype = {
return [leftBoundary, rightBoundary];
},
- _setCornerChildbox: function(childbox, x1, x2, y1, y2) {
- childbox.x1 = x1;
- childbox.x2 = x2;
- childbox.y1 = y1;
- childbox.y2 = y2;
- return;
- },
-
_setVertChildbox: function(childbox, y1, y2) {
childbox.y1 = y1;
@@ -3552,12 +3054,6 @@ Panel.prototype = {
},
_allocate: function(actor, box, flags) {
-
- let cornerMinWidth = 0;
- let cornerWidth = 0;
- let cornerMinHeight = 0;
- let cornerHeight = 0;
-
let allocHeight = box.y2 - box.y1;
let allocWidth = box.x2 - box.x1;
@@ -3591,38 +3087,6 @@ Panel.prototype = {
this._setVertChildbox (childBox, rightBoundary, box.y2);
this._rightBox.allocate(childBox, flags);
-
- // Corners are in response to a bit of optional css and are about painting corners just outside the panels so as to create a seamless
- // visual impression for windows with curved corners
- // So ... top left corner wants to be at the bottom left of the top panel. top right wants to be in the correspondingplace on the right
- // Bottom left corner wants to be at the top left of the bottom panel. bottom right in the corresponding place on the right
- // No panel, no corner necessary.
- // If there are vertical panels as well then we want to shift these in by the panel width
- // If there are vertical panels but no horizontal then the corners are top right and left to right of left panel,
- // and same to left of right panel
-
- if (this.drawcorner[0]) {
- [cornerMinWidth, cornerWidth] = this._leftCorner.actor.get_preferred_width(-1);
- [cornerMinHeight, cornerHeight] = this._leftCorner.actor.get_preferred_height(-1);
- if (this.panelPosition === PanelLoc.left) { // left panel
- this._setCornerChildbox(childBox, box.x2, box.x2+cornerWidth, 0, cornerWidth);
- } else { // right panel
- this._setCornerChildbox(childBox, box.x1-cornerWidth, box.x1, 0, cornerWidth);
- }
- this._leftCorner.actor.allocate(childBox, flags);
- }
-
- if (this.drawcorner[1]) {
- [cornerMinWidth, cornerWidth] = this._rightCorner.actor.get_preferred_width(-1);
- [cornerMinHeight, cornerHeight] = this._rightCorner.actor.get_preferred_height(-1);
- if (this.panelPosition === PanelLoc.left) { // left panel
- this._setCornerChildbox(childBox, box.x2, box.x2+cornerWidth, this.actor.height-cornerHeight, this.actor.height);
- } else { // right panel
- this._setCornerChildbox(childBox, box.x1-cornerWidth, box.x1, this.actor.height-cornerHeight, this.actor.height);
- }
- this._rightCorner.actor.allocate(childBox, flags);
- }
-
} else { // horizontal panel
/* Distribute sizes for the allocated width with points relative to
@@ -3639,36 +3103,14 @@ Panel.prototype = {
this._setHorizChildbox (childBox, rightBoundary, box.x2, box.x1, rightBoundary);
this._rightBox.allocate(childBox, flags);
-
- if (this.drawcorner[0]) {
- [cornerMinWidth, cornerWidth] = this._leftCorner.actor.get_preferred_width(-1);
- [cornerMinHeight, cornerHeight] = this._leftCorner.actor.get_preferred_height(-1);
- if (this.panelPosition === PanelLoc.top) { // top panel
- this._setCornerChildbox(childBox, 0, cornerWidth, box.y2, box.y2+cornerHeight);
- } else { // bottom panel
- this._setCornerChildbox(childBox, 0, cornerWidth, box.y1-cornerHeight, box.y2);
- }
- this._leftCorner.actor.allocate(childBox, flags);
- }
-
- if (this.drawcorner[1]) {
- [cornerMinWidth, cornerWidth] = this._rightCorner.actor.get_preferred_width(-1);
- [cornerMinHeight, cornerHeight] = this._rightCorner.actor.get_preferred_height(-1);
- if (this.panelPosition === PanelLoc.top) { // top panel
- this._setCornerChildbox(childBox, this.actor.width-cornerWidth, this.actor.width, box.y2, box.y2+cornerHeight);
- } else { // bottom panel
- this._setCornerChildbox(childBox, this.actor.width-cornerWidth, this.actor.width, box.y1-cornerHeight, box.y1);
- }
- this._rightCorner.actor.allocate(childBox, flags);
- }
}
},
/**
* _panelHasOpenMenus:
- *
+ *
* Checks if panel has open menus in the global.menuStack
- * @returns
+ * @returns
*/
_panelHasOpenMenus: function() {
if (global.menuStack == null || global.menuStack.length == 0)
diff --git a/js/ui/placeholder.js b/js/ui/placeholder.js
new file mode 100644
index 0000000000..99bf8dc6af
--- /dev/null
+++ b/js/ui/placeholder.js
@@ -0,0 +1,100 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const { Clutter, GObject, Pango, St } = imports.gi;
+
+var Placeholder = GObject.registerClass({
+ Properties: {
+ 'icon-name': GObject.ParamSpec.string(
+ 'icon-name', null, null,
+ GObject.ParamFlags.READWRITE |
+ GObject.ParamFlags.CONSTRUCT,
+ null),
+ 'title': GObject.ParamSpec.string(
+ 'title', null, null,
+ GObject.ParamFlags.READWRITE |
+ GObject.ParamFlags.CONSTRUCT,
+ null),
+ 'description': GObject.ParamSpec.string(
+ 'description', null, null,
+ GObject.ParamFlags.READWRITE |
+ GObject.ParamFlags.CONSTRUCT,
+ null),
+ },
+}, class Placeholder extends St.BoxLayout {
+ _init(params) {
+ this._icon = new St.Icon({
+ style_class: 'placeholder-icon',
+ icon_size: 64,
+ icon_type: St.IconType.SYMBOLIC,
+ x_align: Clutter.ActorAlign.CENTER,
+ });
+
+ this._title = new St.Label({
+ style_class: 'placeholder-label',
+ x_align: Clutter.ActorAlign.CENTER,
+ });
+ this._title.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+ this._title.clutter_text.line_wrap = true;
+
+ this._description = new St.Label({
+ style_class: 'placeholder-description',
+ x_align: Clutter.ActorAlign.CENTER,
+ });
+ this._description.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+ this._description.clutter_text.line_wrap = true;
+
+ super._init({
+ style_class: 'placeholder',
+ reactive: false,
+ vertical: true,
+ x_expand: true,
+ y_expand: true,
+ ...params,
+ });
+
+ this.add_child(this._icon);
+ this.add_child(this._title);
+ this.add_child(this._description);
+ }
+
+ get icon_name() {
+ return this._icon.icon_name;
+ }
+
+ set icon_name(iconName) {
+ if (this._icon.icon_name === iconName)
+ return;
+
+ this._icon.icon_name = iconName;
+ this.notify('icon-name');
+ }
+
+ get title() {
+ return this._title.text;
+ }
+
+ set title(title) {
+ if (this._title.text === title)
+ return;
+
+ this._title.text = title;
+ this.notify('title');
+ }
+
+ get description() {
+ return this._description.text;
+ }
+
+ set description(description) {
+ if (this._description.text === description)
+ return;
+
+ if (description === null)
+ this._description.visible = false;
+ else
+ this._description.visible = true;
+
+ this._description.text = description;
+ this.notify('description');
+ }
+});
diff --git a/js/ui/popupMenu.js b/js/ui/popupMenu.js
index 495a01fe51..22054602f7 100644
--- a/js/ui/popupMenu.js
+++ b/js/ui/popupMenu.js
@@ -14,6 +14,7 @@ const Atk = imports.gi.Atk;
const BoxPointer = imports.ui.boxpointer;
const DND = imports.ui.dnd;
const Main = imports.ui.main;
+const Separator = imports.ui.separator;
const SignalManager = imports.misc.signalManager;
const CheckBox = imports.ui.checkBox;
const RadioButton = imports.ui.radioButton;
@@ -498,28 +499,30 @@ var PopupMenuItem = class PopupMenuItem extends PopupBaseMenuItem {
setOrnament(ornamentType, state) {
switch (ornamentType) {
case OrnamentType.CHECK:
- if ((this._ornament.child)&&(!(this._ornament.child._delegate instanceof CheckBox.CheckButton))) {
+ if ((this._ornament.child) && (!(this._ornament.child._delegate instanceof CheckBox.CheckBox))) {
this._ornament.child.destroy();
this._ornament.child = null;
}
if (!this._ornament.child) {
- let switchOrn = new CheckBox.CheckButton(state);
- this._ornament.child = switchOrn.actor;
+ let switchOrn = new CheckBox.CheckBox();
+ switchOrn.set_checked(state);
+ this._ornament.child = switchOrn;
} else {
- this._ornament.child._delegate.setToggleState(state);
+ this._ornament.child.set_checked(state);
}
this._icon = null;
break;
case OrnamentType.DOT:
- if ((this._ornament.child)&&(!(this._ornament.child._delegate instanceof RadioButton.RadioBox))) {
+ if ((this._ornament.child) && (!(this._ornament.child._delegate instanceof RadioButton.RadioButton))) {
this._ornament.child.destroy();
this._ornament.child = null;
}
if (!this._ornament.child) {
- let radioOrn = new RadioButton.RadioBox(state);
- this._ornament.child = radioOrn.actor;
+ let radioOrn = new RadioButton.RadioButton();
+ radioOrn.set_checked(state);
+ this._ornament.child = radioOrn;
} else {
- this._ornament.child._delegate.setToggleState(state);
+ this._ornament.child.set_checked(state);
}
this._icon = null;
break;
@@ -531,31 +534,9 @@ var PopupSeparatorMenuItem = class PopupSeparatorMenuItem extends PopupBaseMenuI
_init () {
super._init.call(this, { reactive: false });
- this._drawingArea = new St.DrawingArea({ style_class: 'popup-separator-menu-item' });
- this.addActor(this._drawingArea, { span: -1, expand: true });
- this._signals.connect(this._drawingArea, 'repaint', Lang.bind(this, this._onRepaint));
- }
-
- _onRepaint(area) {
- let cr = area.get_context();
- let themeNode = area.get_theme_node();
- let [width, height] = area.get_surface_size();
- let margin = themeNode.get_length('-margin-horizontal');
- let gradientHeight = themeNode.get_length('-gradient-height');
- let startColor = themeNode.get_color('-gradient-start');
- let endColor = themeNode.get_color('-gradient-end');
-
- let gradientWidth = (width - margin * 2);
- let gradientOffset = (height - gradientHeight) / 2;
- let pattern = new Cairo.LinearGradient(margin, gradientOffset, width - margin, gradientOffset + gradientHeight);
- pattern.addColorStopRGBA(0, startColor.red / 255, startColor.green / 255, startColor.blue / 255, startColor.alpha / 255);
- pattern.addColorStopRGBA(0.5, endColor.red / 255, endColor.green / 255, endColor.blue / 255, endColor.alpha / 255);
- pattern.addColorStopRGBA(1, startColor.red / 255, startColor.green / 255, startColor.blue / 255, startColor.alpha / 255);
- cr.setSource(pattern);
- cr.rectangle(margin, gradientOffset, gradientWidth, gradientHeight);
- cr.fill();
-
- cr.$dispose();
+ let separator = new Separator.Separator();
+ separator.set_style_class_name('popup-separator-menu-item');
+ this.addActor(separator, { span: -1, expand: true });
}
}
@@ -1164,28 +1145,30 @@ var PopupIndicatorMenuItem = class PopupIndicatorMenuItem extends PopupBaseMenuI
setOrnament(ornamentType, state) {
switch (ornamentType) {
case OrnamentType.CHECK:
- if ((this._ornament.child)&&(!(this._ornament.child._delegate instanceof CheckBox.CheckButton))) {
+ if ((this._ornament.child)&&(!(this._ornament.child._delegate instanceof CheckBox.CheckBox))) {
this._ornament.child.destroy();
this._ornament.child = null;
}
if (!this._ornament.child) {
- let switchOrn = new CheckBox.CheckButton(null, {}, state);
- this._ornament.child = switchOrn.actor;
+ let switchOrn = new CheckBox.CheckBox();
+ switchOrn.set_checked(state);
+ this._ornament.child = switchOrn;
} else {
- this._ornament.child._delegate.setToggleState(state);
+ this._ornament.child.set_checked(state);
}
this._icon = null;
break;
case OrnamentType.DOT:
- if ((this._ornament.child)&&(!(this._ornament.child._delegate instanceof RadioButton.RadioBox))) {
+ if ((this._ornament.child) && (!(this._ornament.child._delegate instanceof RadioButton.RadioButton))) {
this._ornament.child.destroy();
this._ornament.child = null;
}
if (!this._ornament.child) {
- let radioOrn = new RadioButton.RadioBox(state);
- this._ornament.child = radioOrn.actor;
+ let radioOrn = new RadioButton.RadioButton();
+ radioOrn.set_checked(state);
+ this._ornament.child = radioOrn;
} else {
- this._ornament.child._delegate.setToggleState(state);
+ this._ornament.child.set_checked(state);
}
this._icon = null;
break;
@@ -3506,7 +3489,7 @@ var PopupMenuManager = class PopupMenuManager {
}
_grab() {
- if (!Main.pushModal(this._owner.actor)) {
+ if (!Main.pushModal(this._owner.actor, undefined, undefined, Cinnamon.ActionMode.POPUP)) {
return;
}
this._signals.connect(global.stage, 'captured-event', this._onEventCapture, this);
diff --git a/js/ui/radioButton.js b/js/ui/radioButton.js
index 91dfdd969c..09614c3dbd 100644
--- a/js/ui/radioButton.js
+++ b/js/ui/radioButton.js
@@ -1,196 +1,41 @@
const Clutter = imports.gi.Clutter;
+const GObject = imports.gi.GObject;
const Pango = imports.gi.Pango;
-const Cinnamon = imports.gi.Cinnamon;
const St = imports.gi.St;
-const Signals = imports.signals;
-const Lang = imports.lang;
-
-function RadioButtonContainer() {
- this._init();
-}
-RadioButtonContainer.prototype = {
- _init: function() {
- this.actor = new Cinnamon.GenericContainer({ y_align: St.Align.MIDDLE });
- this.actor.connect('get-preferred-width',
- Lang.bind(this, this._getPreferredWidth));
- this.actor.connect('get-preferred-height',
- Lang.bind(this, this._getPreferredHeight));
- this.actor.connect('allocate',
- Lang.bind(this, this._allocate));
- this.actor.connect('style-changed', Lang.bind(this,
- function() {
- let node = this.actor.get_theme_node();
- this._spacing = node.get_length('spacing');
- }));
- this.actor.request_mode = Clutter.RequestMode.HEIGHT_FOR_WIDTH;
+var RadioButton = GObject.registerClass(
+class RadioButton extends St.Button {
+ _init(label) {
+ let container = new St.BoxLayout();
+ super._init({
+ style_class: 'radiobutton',
+ important: true,
+ child: container,
+ button_mask: St.ButtonMask.ONE,
+ toggle_mode: true,
+ can_focus: true,
+ x_fill: true,
+ y_fill: true,
+ });
this._box = new St.Bin();
- this.actor.add_actor(this._box);
-
- this.label = new St.Label();
- this.label.clutter_text.set_line_wrap(false);
- this.label.clutter_text.set_ellipsize(Pango.EllipsizeMode.NONE);
- this.actor.add_actor(this.label);
-
- this._spacing = 0;
- },
-
- _getPreferredWidth: function(actor, forHeight, alloc) {
- let [minWidth, natWidth] = this._box.get_preferred_width(forHeight);
-
- alloc.min_size = minWidth + this._spacing;
- alloc.natural_size = natWidth + this._spacing;
- },
-
- _getPreferredHeight: function(actor, forWidth, alloc) {
- let [minBoxHeight, natBoxHeight] =
- this._box.get_preferred_height(-1);
- let [minLabelHeight, natLabelHeight] =
- this.label.get_preferred_height(-1);
-
- alloc.min_size = Math.max(minBoxHeight, 2 * minLabelHeight);
- alloc.natural_size = Math.max(natBoxHeight, 2 * natLabelHeight);
- },
-
- _allocate: function(actor, box, flags) {
- let availWidth = box.x2 - box.x1;
- let availHeight = box.y2 - box.y1;
-
- let childBox = new Clutter.ActorBox();
- let [minBoxWidth, natBoxWidth] =
- this._box.get_preferred_width(-1);
- let [minBoxHeight, natBoxHeight] =
- this._box.get_preferred_height(-1);
- childBox.x1 = box.x1;
- childBox.x2 = box.x1 + natBoxWidth;
- childBox.y1 = box.y1;
- childBox.y2 = box.y1 + natBoxHeight;
- this._box.allocate(childBox, flags);
-
- childBox.x1 = box.x1 + natBoxWidth + this._spacing;
- childBox.x2 = availWidth - childBox.x1;
- childBox.y1 = box.y1;
- childBox.y2 = box.y2;
- this.label.allocate(childBox, flags);
- }
-};
-
-function RadioBox(state) {
- this._init(state);
-}
-
-RadioBox.prototype = {
- _init: function(state) {
- this.actor = new St.Button({ style_class: 'radiobutton',
- button_mask: St.ButtonMask.ONE,
- toggle_mode: true,
- can_focus: true,
- x_fill: true,
- y_fill: true,
- y_align: St.Align.MIDDLE });
-
- this.actor._delegate = this;
- this.actor.checked = state;
- this._container = new St.Bin();
- this.actor.set_child(this._container);
- },
-
- setToggleState: function(state) {
- this.actor.checked = state;
- },
-
- toggle: function() {
- this.setToggleState(!this.actor.checked);
- },
+ this._box.set_y_align(Clutter.ActorAlign.START);
+ container.add_child(this._box);
- destroy: function() {
- this.actor.destroy();
- }
-};
-
-function RadioButton(label) {
- this._init(label);
-}
-
-RadioButton.prototype = {
- __proto__: RadioBox.prototype,
-
- _init: function(label) {
- RadioBox.prototype._init.call(this, false);
- this._container.destroy();
- this._container = new RadioButtonContainer();
- this.actor.set_child(this._container.actor);
+ this._label = new St.Label({ y_align: Clutter.ActorAlign.CENTER });
+ this._label.clutter_text.set_line_wrap(true);
+ this._label.clutter_text.set_ellipsize(Pango.EllipsizeMode.NONE);
+ container.add_child(this._label);
if (label)
this.setLabel(label);
- },
-
- setLabel: function(label) {
- this._container.label.set_text(label);
- },
-
- getLabelActor: function() {
- return this._container.label;
}
-};
-
-function RadioButtonGroup() {
- this._init();
-}
-
-RadioButtonGroup.prototype = {
- _init: function() {
- this.actor = new St.BoxLayout({ vertical: true, width: 250 });
- this._buttons = [];
- this._activeId = null;
- },
- addButton: function(buttonId, label) {
- this.radioButton = new RadioButton(label);
- this.radioButton.actor.connect("clicked",
- Lang.bind(this, function(actor) {
- this.buttonClicked(actor, buttonId);
- }));
-
- this._buttons.push({ id: buttonId, button: this.radioButton });
- this.actor.add(this.radioButton.actor, { x_fill: true, y_fill: false, y_align: St.Align.MIDDLE });
- },
-
- radioChanged: function(actor) {
-
- },
-
- buttonClicked: function(actor, buttonId) {
- for (const button of this._buttons) {
- if (buttonId !== button['id'] && button['button'].actor.checked) {
- button['button'].actor.checked = false;
- }
- else if (buttonId === button['id'] && !button['button'].actor.checked) {
- button['button'].actor.checked = true;
- }
- }
-
- // Only trigger real changes to radio selection.
- if (buttonId !== this._activeId) {
- this._activeId = buttonId;
- this.emit('radio-changed', this._activeId);
- }
- },
-
- setActive: function(buttonId) {
- for (const button of this._buttons) {
- button['button'].actor.checked = buttonId === button['id'];
- }
-
- if (this._activeId != buttonId) {
- this._activeId = buttonId;
- this.emit('radio-changed', this._activeId);
- }
- },
+ setLabel(label) {
+ this._label.set_text(label);
+ }
- getActive: function() {
- return this._activeId;
- }
-}
-Signals.addSignalMethods(RadioButtonGroup.prototype);
+ getLabelActor() {
+ return this._label;
+ }
+});
diff --git a/js/ui/screensaver/albumArtWidget.js b/js/ui/screensaver/albumArtWidget.js
new file mode 100644
index 0000000000..d65985eabf
--- /dev/null
+++ b/js/ui/screensaver/albumArtWidget.js
@@ -0,0 +1,631 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+//
+// albumArtWidget.js - Album art widget for screensaver
+//
+// Displays album art, track info, and playback controls when music is playing.
+//
+
+const Clutter = imports.gi.Clutter;
+const Cvc = imports.gi.Cvc;
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const GObject = imports.gi.GObject;
+const Pango = imports.gi.Pango;
+const St = imports.gi.St;
+
+const MprisPlayer = imports.misc.mprisPlayer;
+const ScreensaverWidget = imports.ui.screensaver.screensaverWidget;
+const SignalManager = imports.misc.signalManager;
+const Slider = imports.ui.slider;
+
+const SCREENSAVER_SCHEMA = 'org.cinnamon.desktop.screensaver';
+const ALBUM_ART_SIZE_BASE = 300;
+const CONTROL_ICON_SIZE_BASE = 24;
+
+var AlbumArtWidget = GObject.registerClass(
+class AlbumArtWidget extends ScreensaverWidget.ScreensaverWidget {
+ _init() {
+ super._init({
+ style_class: 'albumart-widget',
+ vertical: true,
+ x_expand: false,
+ y_expand: false
+ });
+
+ this._settings = new Gio.Settings({ schema_id: SCREENSAVER_SCHEMA });
+ this._showAlbumArt = this._settings.get_boolean('show-album-art');
+ this._allowMediaControl = this._settings.get_boolean('allow-media-control');
+
+ if (!this._showAlbumArt) {
+ this.hide();
+ return;
+ }
+
+ this._artSize = ALBUM_ART_SIZE_BASE * global.ui_scale;
+ this._controlIconSize = CONTROL_ICON_SIZE_BASE;
+
+ this.setAwakePosition(0, St.Align.END, St.Align.MIDDLE);
+
+ this._signalManager = new SignalManager.SignalManager(null);
+ this._mprisManager = MprisPlayer.getMprisPlayerManager();
+ this._currentPlayer = null;
+ this._currentArtUrl = null;
+ this._coverFileTmp = null;
+ this._coverLoadHandle = 0;
+
+ if (this._allowMediaControl) {
+ this._volumeControl = new Cvc.MixerControl({ name: 'Cinnamon Screensaver' });
+ this._volumeNorm = this._volumeControl.get_vol_max_norm();
+ this._outputStream = null;
+ this._outputVolumeId = 0;
+ this._outputMutedId = 0;
+ }
+
+ this._buildUI();
+ this._connectToManager();
+
+ if (this._allowMediaControl) {
+ this._setupVolumeControl();
+ }
+ }
+
+ _setupVolumeControl() {
+ this._signalManager.connect(this._volumeControl, 'state-changed',
+ this._onVolumeControlStateChanged.bind(this));
+ this._signalManager.connect(this._volumeControl, 'default-sink-changed',
+ this._onDefaultSinkChanged.bind(this));
+ this._volumeControl.open();
+ }
+
+ _onVolumeControlStateChanged() {
+ if (this._volumeControl.get_state() === Cvc.MixerControlState.READY) {
+ this._onDefaultSinkChanged();
+ }
+ }
+
+ _onDefaultSinkChanged() {
+ if (this._outputStream) {
+ if (this._outputVolumeId) {
+ this._outputStream.disconnect(this._outputVolumeId);
+ this._outputVolumeId = 0;
+ }
+ if (this._outputMutedId) {
+ this._outputStream.disconnect(this._outputMutedId);
+ this._outputMutedId = 0;
+ }
+ }
+
+ this._outputStream = this._volumeControl.get_default_sink();
+
+ if (this._outputStream) {
+ this._outputVolumeId = this._outputStream.connect('notify::volume',
+ this._updateVolumeSlider.bind(this));
+ this._outputMutedId = this._outputStream.connect('notify::is-muted',
+ this._updateVolumeSlider.bind(this));
+ this._updateVolumeSlider();
+ }
+ }
+
+ _updateVolumeSlider() {
+ if (!this._outputStream) return;
+
+ let muted = this._outputStream.is_muted;
+ let volume = muted ? 0 : this._outputStream.volume / this._volumeNorm;
+
+ this._volumeSlider.setValue(Math.min(1, volume));
+ this._updateVolumeIcon(volume, muted);
+ }
+
+ _updateVolumeIcon(volume, muted) {
+ let iconName;
+ if (muted || volume <= 0) {
+ iconName = 'audio-volume-muted-symbolic';
+ } else if (volume <= 0.33) {
+ iconName = 'audio-volume-low-symbolic';
+ } else if (volume <= 0.66) {
+ iconName = 'audio-volume-medium-symbolic';
+ } else {
+ iconName = 'audio-volume-high-symbolic';
+ }
+ this._volumeIcon.icon_name = iconName;
+ }
+
+ _onVolumeChanged(slider, value) {
+ if (!this._outputStream) return;
+
+ let volume = value * this._volumeNorm;
+
+ // Snap to 100% if close
+ if (volume !== this._volumeNorm &&
+ volume > this._volumeNorm * 0.975 &&
+ volume < this._volumeNorm * 1.025) {
+ volume = this._volumeNorm;
+ }
+
+ this._outputStream.volume = volume;
+ this._outputStream.push_volume();
+
+ if (this._outputStream.is_muted && volume > 0) {
+ this._outputStream.change_is_muted(false);
+ }
+
+ this._updateVolumeIcon(value, false);
+ }
+
+ _buildUI() {
+ this._infoContainer = new St.BoxLayout({
+ style_class: 'albumart-info-container',
+ vertical: true,
+ x_align: Clutter.ActorAlign.CENTER
+ });
+ this.add_child(this._infoContainer);
+
+ // Art container using FixedLayout to overlay track info on album art
+ this._artContainer = new St.Widget({
+ layout_manager: new Clutter.FixedLayout(),
+ width: this._artSize,
+ height: this._artSize
+ });
+ this._infoContainer.add_child(this._artContainer);
+
+ this._artBin = new St.Bin({
+ style_class: 'albumart-cover-bin',
+ width: this._artSize,
+ height: this._artSize
+ });
+ this._artContainer.add_child(this._artBin);
+ this._showDefaultArt();
+
+ // Track info overlay - anchored to bottom of album art
+ this._trackInfoBox = new St.BoxLayout({
+ style_class: 'albumart-track-info-overlay',
+ vertical: true,
+ width: this._artSize
+ });
+
+ this._trackInfoBox.connect('notify::height', () => {
+ this._trackInfoBox.set_position(0, this._artSize - this._trackInfoBox.height);
+ });
+ this._artContainer.add_child(this._trackInfoBox);
+
+ this._titleLabel = new St.Label({
+ style_class: 'albumart-title-overlay',
+ x_align: Clutter.ActorAlign.CENTER
+ });
+ this._titleLabel.clutter_text.ellipsize = Pango.EllipsizeMode.END;
+ this._titleLabel.clutter_text.line_wrap = false;
+ this._trackInfoBox.add_child(this._titleLabel);
+
+ this._artistLabel = new St.Label({
+ style_class: 'albumart-artist-overlay',
+ x_align: Clutter.ActorAlign.CENTER
+ });
+ this._artistLabel.clutter_text.ellipsize = Pango.EllipsizeMode.END;
+ this._artistLabel.clutter_text.line_wrap = false;
+ this._trackInfoBox.add_child(this._artistLabel);
+
+ this._albumLabel = new St.Label({
+ style_class: 'albumart-album-overlay',
+ x_align: Clutter.ActorAlign.CENTER
+ });
+ this._albumLabel.clutter_text.ellipsize = Pango.EllipsizeMode.END;
+ this._albumLabel.clutter_text.line_wrap = false;
+ this._trackInfoBox.add_child(this._albumLabel);
+
+ if (this._allowMediaControl) {
+ this._controlsBox = new St.BoxLayout({
+ style_class: 'albumart-controls',
+ x_align: Clutter.ActorAlign.CENTER
+ });
+
+ this._prevButton = this._createControlButton(
+ 'media-skip-backward-symbolic',
+ () => this._onPrevious()
+ );
+ this._controlsBox.add_child(this._prevButton);
+
+ this._playPauseButton = this._createControlButton(
+ 'media-playback-start-symbolic',
+ () => this._onPlayPause()
+ );
+ this._controlsBox.add_child(this._playPauseButton);
+
+ this._nextButton = this._createControlButton(
+ 'media-skip-forward-symbolic',
+ () => this._onNext()
+ );
+ this._controlsBox.add_child(this._nextButton);
+
+ this._infoContainer.add_child(this._controlsBox);
+
+ this._volumeBox = new St.BoxLayout({
+ style_class: 'albumart-volume-box',
+ x_align: Clutter.ActorAlign.CENTER
+ });
+
+ this._volumeIcon = new St.Icon({
+ icon_name: 'audio-volume-medium-symbolic',
+ icon_type: St.IconType.SYMBOLIC,
+ style_class: 'albumart-volume-icon'
+ });
+ this._volumeBox.add_child(this._volumeIcon);
+
+ this._volumeSlider = new Slider.Slider(0);
+ this._volumeSlider.actor.style_class = 'albumart-volume-slider';
+ this._volumeSlider.connect('value-changed', this._onVolumeChanged.bind(this));
+ this._volumeBox.add_child(this._volumeSlider.actor);
+
+ this._infoContainer.add_child(this._volumeBox);
+
+ this._controlsBox.hide();
+ this._volumeBox.hide();
+ }
+
+ this.hide();
+ }
+
+ _createControlButton(iconName, callback) {
+ let button = new St.Button({
+ style_class: 'albumart-control-button',
+ can_focus: true,
+ child: new St.Icon({
+ icon_name: iconName,
+ icon_type: St.IconType.SYMBOLIC,
+ icon_size: this._controlIconSize
+ })
+ });
+ button.connect('clicked', callback.bind(this));
+ return button;
+ }
+
+ _connectToManager() {
+ this._signalManager.connect(this._mprisManager, 'player-added',
+ this._onPlayerAdded.bind(this));
+ this._signalManager.connect(this._mprisManager, 'player-removed',
+ this._onPlayerRemoved.bind(this));
+
+ this._updateCurrentPlayer();
+ }
+
+ _onPlayerAdded(manager, player) {
+ this._updateCurrentPlayer();
+ }
+
+ _onPlayerRemoved(manager, busName, owner) {
+ if (this._currentPlayer && this._currentPlayer.getOwner() === owner) {
+ this._disconnectFromPlayer();
+ this._updateCurrentPlayer();
+ }
+ }
+
+ _updateCurrentPlayer() {
+ let newPlayer = this._mprisManager.getBestPlayer();
+
+ if (newPlayer === this._currentPlayer) {
+ if (this._currentPlayer) {
+ this._updateDisplay();
+ }
+ return;
+ }
+
+ this._disconnectFromPlayer();
+
+ this._currentPlayer = newPlayer;
+
+ if (this._currentPlayer) {
+ this._connectToPlayer();
+ this._updateDisplay();
+ this.show();
+ } else {
+ this.hide();
+ }
+ }
+
+ _connectToPlayer() {
+ if (!this._currentPlayer) return;
+
+ this._signalManager.connect(this._currentPlayer, 'metadata-changed',
+ this._onMetadataChanged.bind(this));
+ this._signalManager.connect(this._currentPlayer, 'status-changed',
+ this._onStatusChanged.bind(this));
+ this._signalManager.connect(this._currentPlayer, 'capabilities-changed',
+ this._updateControls.bind(this));
+ this._signalManager.connect(this._currentPlayer, 'closed',
+ this._onPlayerClosed.bind(this));
+ }
+
+ _disconnectFromPlayer() {
+ if (!this._currentPlayer) return;
+
+ this._signalManager.disconnect('metadata-changed', this._currentPlayer);
+ this._signalManager.disconnect('status-changed', this._currentPlayer);
+ this._signalManager.disconnect('capabilities-changed', this._currentPlayer);
+ this._signalManager.disconnect('closed', this._currentPlayer);
+
+ this._currentPlayer = null;
+ this._currentArtUrl = null;
+ }
+
+ _onMetadataChanged() {
+ this._updateDisplay();
+ }
+
+ _onStatusChanged(player, status) {
+ this._updatePlayPauseButton();
+ this._updateCurrentPlayer();
+ }
+
+ _onPlayerClosed() {
+ this._disconnectFromPlayer();
+ this._updateCurrentPlayer();
+ }
+
+ _updateDisplay() {
+ if (!this._currentPlayer) return;
+
+ let title = this._currentPlayer.getTitle();
+ let artist = this._currentPlayer.getArtist();
+ let album = this._currentPlayer.getAlbum();
+
+ this._titleLabel.text = title || _("Unknown Title");
+ this._artistLabel.text = artist || _("Unknown Artist");
+ this._albumLabel.text = album || "";
+ this._albumLabel.visible = (album !== "");
+
+ let artUrl = this._currentPlayer.getProcessedArtUrl();
+
+ if (artUrl !== this._currentArtUrl) {
+ this._currentArtUrl = artUrl;
+ this._loadAlbumArt(artUrl);
+ }
+
+ this._updateControls();
+ this._updatePlayPauseButton();
+ }
+
+ _updateControls() {
+ if (!this._allowMediaControl || !this._prevButton)
+ return;
+
+ if (!this._currentPlayer) {
+ this._prevButton.reactive = false;
+ this._playPauseButton.reactive = false;
+ this._nextButton.reactive = false;
+ return;
+ }
+
+ this._prevButton.reactive = this._currentPlayer.canGoPrevious();
+ this._playPauseButton.reactive = this._currentPlayer.canControl();
+ this._nextButton.reactive = this._currentPlayer.canGoNext();
+ }
+
+ _updatePlayPauseButton() {
+ if (!this._allowMediaControl || !this._playPauseButton)
+ return;
+
+ if (!this._currentPlayer) return;
+
+ let iconName = this._currentPlayer.isPlaying() ?
+ 'media-playback-pause-symbolic' : 'media-playback-start-symbolic';
+
+ this._playPauseButton.child.icon_name = iconName;
+ }
+
+ _loadAlbumArt(url) {
+ if (!url || url === "") {
+ this._showDefaultArt();
+ return;
+ }
+
+ if (url.match(/^https?:\/\//)) {
+ // Remote URL - download it
+ this._downloadAlbumArt(url);
+ } else if (url.match(/^file:\/\//)) {
+ this._loadLocalArt(url);
+ } else if (url.match(/^data:image\//)) {
+ // Base64 data URL
+ this._loadBase64Art(url);
+ } else {
+ this._showDefaultArt();
+ }
+ }
+
+ _ensureTempFile() {
+ this._cleanupTempFile();
+
+ try {
+ let [file, iostream] = Gio.file_new_tmp('XXXXXX.albumart-cover');
+ iostream.close(null);
+ this._coverFileTmp = file;
+ return true;
+ } catch (e) {
+ global.logError(`AlbumArtWidget: Failed to create temp file: ${e}`);
+ this._showDefaultArt();
+ return false;
+ }
+ }
+
+ _showArtFromPath(path) {
+ this._coverLoadHandle = St.TextureCache.get_default().load_image_from_file_async(
+ path,
+ this._artSize,
+ this._artSize,
+ this._onCoverLoaded.bind(this)
+ );
+ }
+
+ _onCoverLoaded(cache, handle, actor) {
+ if (handle !== this._coverLoadHandle) {
+ return;
+ }
+
+ if (actor) {
+ this._artBin.set_child(actor);
+ } else {
+ this._showDefaultArt();
+ }
+ }
+
+ _loadLocalArt(url) {
+ let file = Gio.File.new_for_uri(url);
+ file.query_info_async(
+ Gio.FILE_ATTRIBUTE_STANDARD_TYPE,
+ Gio.FileQueryInfoFlags.NONE,
+ GLib.PRIORITY_DEFAULT,
+ null,
+ (f, result) => {
+ try {
+ f.query_info_finish(result);
+ this._showArtFromPath(f.get_path());
+ } catch (e) {
+ this._showDefaultArt();
+ }
+ }
+ );
+ }
+
+ _downloadAlbumArt(url) {
+ if (!this._ensureTempFile())
+ return;
+
+ let src = Gio.File.new_for_uri(url);
+ src.copy_async(
+ this._coverFileTmp,
+ Gio.FileCopyFlags.OVERWRITE,
+ GLib.PRIORITY_DEFAULT,
+ null, null,
+ (source, result) => {
+ try {
+ source.copy_finish(result);
+ this._showArtFromPath(this._coverFileTmp.get_path());
+ } catch (e) {
+ global.logWarning(`AlbumArtWidget: Failed to download album art: ${e.message}`);
+ this._showDefaultArt();
+ }
+ }
+ );
+ }
+
+ _loadBase64Art(dataUrl) {
+ let match = dataUrl.match(/^data:image\/(png|jpeg|jpg);base64,(.+)$/);
+ if (!match) {
+ this._showDefaultArt();
+ return;
+ }
+
+ if (!this._ensureTempFile())
+ return;
+
+ let decoded;
+ try {
+ decoded = GLib.base64_decode(match[2]);
+ } catch (e) {
+ global.logError(`AlbumArtWidget: Failed to decode base64 art: ${e}`);
+ this._showDefaultArt();
+ return;
+ }
+
+ let bytes = new GLib.Bytes(decoded);
+ this._coverFileTmp.replace_contents_bytes_async(
+ bytes,
+ null,
+ false,
+ Gio.FileCreateFlags.REPLACE_DESTINATION,
+ null,
+ (file, result) => {
+ try {
+ file.replace_contents_finish(result);
+ this._showArtFromPath(this._coverFileTmp.get_path());
+ } catch (e) {
+ global.logError(`AlbumArtWidget: Failed to write base64 art: ${e}`);
+ this._showDefaultArt();
+ }
+ }
+ );
+ }
+
+ _showDefaultArt() {
+ let defaultIcon = new St.Icon({
+ icon_name: 'media-optical',
+ icon_size: this._artSize,
+ icon_type: St.IconType.FULLCOLOR
+ });
+ this._artBin.set_child(defaultIcon);
+ }
+
+ _onPrevious() {
+ if (this._currentPlayer) {
+ this._currentPlayer.previous();
+ }
+ }
+
+ _onPlayPause() {
+ if (this._currentPlayer) {
+ this._currentPlayer.playPause();
+ }
+ }
+
+ _onNext() {
+ if (this._currentPlayer) {
+ this._currentPlayer.next();
+ }
+ }
+
+ onScreensaverActivated() {
+ this._updateCurrentPlayer();
+ }
+
+ onScreensaverDeactivated() {
+ this._cleanupTempFile();
+ }
+
+ onAwake() {
+ if (this._allowMediaControl && this._controlsBox) {
+ this._controlsBox.show();
+ this._volumeBox.show();
+ this._infoContainer.add_style_pseudo_class('awake');
+ }
+ }
+
+ onSleep() {
+ if (this._allowMediaControl && this._controlsBox) {
+ this._controlsBox.hide();
+ this._volumeBox.hide();
+ this._infoContainer.remove_style_pseudo_class('awake');
+ }
+ }
+
+ _cleanupTempFile() {
+ if (this._coverFileTmp) {
+ try {
+ this._coverFileTmp.delete(null);
+ } catch (e) {
+ // Ignore - file may not exist
+ }
+ this._coverFileTmp = null;
+ }
+ }
+
+ destroy() {
+ if (this._signalManager) {
+ this._signalManager.disconnectAllSignals();
+ }
+ this._disconnectFromPlayer();
+ this._cleanupTempFile();
+
+ if (this._outputStream) {
+ if (this._outputVolumeId) {
+ this._outputStream.disconnect(this._outputVolumeId);
+ }
+ if (this._outputMutedId) {
+ this._outputStream.disconnect(this._outputMutedId);
+ }
+ }
+ if (this._volumeControl) {
+ this._volumeControl.close();
+ this._volumeControl = null;
+ }
+
+ super.destroy();
+ }
+});
diff --git a/js/ui/screensaver/awayMessageDialog.js b/js/ui/screensaver/awayMessageDialog.js
new file mode 100644
index 0000000000..0a1d5aa1ef
--- /dev/null
+++ b/js/ui/screensaver/awayMessageDialog.js
@@ -0,0 +1,68 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const Clutter = imports.gi.Clutter;
+const St = imports.gi.St;
+const GObject = imports.gi.GObject;
+
+const Dialog = imports.ui.dialog;
+const ModalDialog = imports.ui.modalDialog;
+
+/**
+ * AwayMessageDialog:
+ *
+ * A modal dialog that prompts the user for an away message before locking
+ * the screen. Used when org.cinnamon.desktop.screensaver 'ask-for-away-message'
+ * is enabled.
+ */
+var AwayMessageDialog = GObject.registerClass(
+class AwayMessageDialog extends ModalDialog.ModalDialog {
+ _init(callback) {
+ super._init();
+
+ this._callback = callback;
+
+ let content = new Dialog.MessageDialogContent({
+ title: _("Lock Screen"),
+ description: _("Please type an away message for the lock screen")
+ });
+ this.contentLayout.add_child(content);
+
+ this._entry = new St.Entry({
+ style_class: 'prompt-dialog-password-entry',
+ hint_text: _("Away message"),
+ can_focus: true,
+ x_expand: true
+ });
+ this.contentLayout.add_child(this._entry);
+
+ this._entry.clutter_text.connect('activate', this._onLock.bind(this));
+ this.setInitialKeyFocus(this._entry);
+
+ // Buttons
+ this.setButtons([
+ {
+ label: _("Cancel"),
+ action: this._onCancel.bind(this),
+ key: Clutter.KEY_Escape
+ },
+ {
+ label: _("Lock"),
+ action: this._onLock.bind(this),
+ default: true
+ }
+ ]);
+ }
+
+ _onCancel() {
+ this.close();
+ }
+
+ _onLock() {
+ let message = this._entry.get_text();
+ this.close();
+
+ if (this._callback) {
+ this._callback(message);
+ }
+ }
+});
diff --git a/js/ui/screensaver/clockWidget.js b/js/ui/screensaver/clockWidget.js
new file mode 100644
index 0000000000..33d5d3872b
--- /dev/null
+++ b/js/ui/screensaver/clockWidget.js
@@ -0,0 +1,126 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const CinnamonDesktop = imports.gi.CinnamonDesktop;
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const GObject = imports.gi.GObject;
+const St = imports.gi.St;
+const Clutter = imports.gi.Clutter;
+
+const ScreensaverWidget = imports.ui.screensaver.screensaverWidget;
+
+const SCREENSAVER_SCHEMA = 'org.cinnamon.desktop.screensaver';
+
+var ClockWidget = GObject.registerClass(
+class ClockWidget extends ScreensaverWidget.ScreensaverWidget {
+ _init(awayMessage) {
+ super._init({
+ style_class: 'clock-widget',
+ vertical: true,
+ x_expand: false,
+ y_expand: false
+ });
+
+ this.setAwakePosition(0, St.Align.START, St.Align.MIDDLE);
+
+ this._settings = new Gio.Settings({ schema_id: SCREENSAVER_SCHEMA });
+ this._awayMessage = awayMessage;
+ this._showClock = this._settings.get_boolean('show-clock');
+
+ this._timeLabel = new St.Label({
+ style_class: 'clock-time-label',
+ x_align: Clutter.ActorAlign.CENTER
+ });
+ this.add_child(this._timeLabel);
+
+ this._dateLabel = new St.Label({
+ style_class: 'clock-date-label',
+ x_align: Clutter.ActorAlign.CENTER
+ });
+ this._dateLabel.clutter_text.line_wrap = true;
+ this.add_child(this._dateLabel);
+
+ this._messageLabel = new St.Label({
+ style_class: 'clock-message-label',
+ x_align: Clutter.ActorAlign.CENTER
+ });
+ this._messageLabel.clutter_text.line_wrap = true;
+ this.add_child(this._messageLabel);
+
+ this._messageAuthor = new St.Label({
+ style_class: 'clock-message-author',
+ x_align: Clutter.ActorAlign.CENTER
+ });
+ this.add_child(this._messageAuthor);
+
+ this._wallClock = new CinnamonDesktop.WallClock();
+ this._wallClock.connect('notify::clock', this._updateClock.bind(this));
+
+ this._setClockFormat();
+ this._updateClock();
+ }
+
+ _setClockFormat() {
+ if (this._settings.get_boolean('use-custom-format')) {
+ this._dateFormat = this._settings.get_string('date-format') || '%A %B %-e';
+ this._timeFormat = this._settings.get_string('time-format') || '%H:%M';
+ } else {
+ this._dateFormat = this._wallClock.get_default_date_format();
+ this._timeFormat = this._wallClock.get_default_time_format();
+
+ // %l is 12-hr hours, but it adds a space to 0-9, which looks bad
+ // The '-' modifier tells the GDateTime formatter not to pad the value
+ this._timeFormat = this._timeFormat.replace('%l', '%-l');
+ }
+
+ this._wallClock.set_format_string(this._timeFormat);
+ }
+
+ _updateClock() {
+ this._timeLabel.text = this._wallClock.get_clock();
+
+ let now = GLib.DateTime.new_now_local();
+ this._dateLabel.text = now.format(this._dateFormat);
+
+ if (this._awayMessage && this._awayMessage !== '') {
+ this._messageLabel.text = this._awayMessage;
+ this._messageAuthor.text = ` ~ ${GLib.get_real_name()}`;
+ this._messageLabel.visible = true;
+ this._messageAuthor.visible = true;
+ } else {
+ let defaultMessage = this._settings.get_string('default-message');
+ if (defaultMessage && defaultMessage !== '') {
+ this._messageLabel.text = defaultMessage;
+ this._messageLabel.visible = true;
+ } else {
+ this._messageLabel.visible = false;
+ }
+ this._messageAuthor.visible = false;
+ }
+ }
+
+ onScreensaverActivated() {
+ if (!this._showClock) {
+ this.hide();
+ }
+ }
+
+ onAwake() {
+ this.show();
+ }
+
+ onSleep() {
+ if (!this._showClock) {
+ this.hide();
+ }
+ }
+
+ destroy() {
+ if (this._wallClock) {
+ this._wallClock.run_dispose();
+ this._wallClock = null;
+ }
+
+ super.destroy();
+ }
+});
diff --git a/js/ui/screensaver/infoPanel.js b/js/ui/screensaver/infoPanel.js
new file mode 100644
index 0000000000..3618347ddd
--- /dev/null
+++ b/js/ui/screensaver/infoPanel.js
@@ -0,0 +1,119 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const GObject = imports.gi.GObject;
+const Gio = imports.gi.Gio;
+const St = imports.gi.St;
+
+const NotificationWidget = imports.ui.screensaver.notificationWidget;
+const PowerWidget = imports.ui.screensaver.powerWidget;
+
+const SCREENSAVER_SCHEMA = 'org.cinnamon.desktop.screensaver';
+
+var InfoPanel = GObject.registerClass(
+class InfoPanel extends St.BoxLayout {
+ _init() {
+ super._init({
+ style_class: 'info-panel',
+ x_expand: false,
+ y_expand: false,
+ vertical: false
+ });
+
+ this._awake = false;
+ this._enabled = false;
+ this._notificationWidget = null;
+ this._powerWidget = null;
+
+ let settings = new Gio.Settings({ schema_id: SCREENSAVER_SCHEMA });
+ this._enabled = settings.get_boolean('show-info-panel');
+
+ if (!this._enabled) {
+ this.hide();
+ return;
+ }
+
+ this._notificationWidget = new NotificationWidget.NotificationWidget();
+ this._notificationWidget.connect('count-changed', this._updateVisibility.bind(this));
+ this.add_child(this._notificationWidget);
+
+ this._powerWidget = new PowerWidget.PowerWidget();
+ this._powerWidget.connect('power-state-changed', this._updateVisibility.bind(this));
+ this.add_child(this._powerWidget);
+
+ this._updateVisibility();
+ }
+
+ onScreensaverActivated() {
+ if (!this._enabled)
+ return;
+
+ if (this._notificationWidget) {
+ this._notificationWidget.reset();
+ this._notificationWidget.activate();
+ }
+ }
+
+ onScreensaverDeactivated() {
+ if (!this._enabled)
+ return;
+
+ if (this._notificationWidget) {
+ this._notificationWidget.deactivate();
+ }
+ }
+
+ onWake() {
+ this._awake = true;
+ this._updateVisibility();
+ }
+
+ onSleep() {
+ this._awake = false;
+ this._updateVisibility();
+ }
+
+ _updateVisibility() {
+ if (!this._enabled) {
+ this.hide();
+ return;
+ }
+
+ let hasNotifications = this._notificationWidget && this._notificationWidget.shouldShow();
+ let hasPower = this._powerWidget && this._powerWidget.shouldShow();
+ let hasCriticalBattery = this._powerWidget && this._powerWidget.isBatteryCritical();
+
+ if (this._awake) {
+ // When awake, show panel if either child has content
+ if (this._powerWidget)
+ this._powerWidget.visible = hasPower;
+ if (this._notificationWidget)
+ this._notificationWidget.visible = hasNotifications;
+
+ this.visible = hasNotifications || hasPower;
+ } else {
+ // When sleeping, show notifications always but power only if critical
+ if (this._notificationWidget)
+ this._notificationWidget.visible = hasNotifications;
+ if (this._powerWidget)
+ this._powerWidget.visible = hasCriticalBattery;
+
+ this.visible = hasNotifications || hasCriticalBattery;
+ }
+ }
+
+ destroy() {
+ this.onScreensaverDeactivated();
+
+ if (this._notificationWidget) {
+ this._notificationWidget.destroy();
+ this._notificationWidget = null;
+ }
+
+ if (this._powerWidget) {
+ this._powerWidget.destroy();
+ this._powerWidget = null;
+ }
+
+ super.destroy();
+ }
+});
diff --git a/js/ui/screensaver/nameBlocker.js b/js/ui/screensaver/nameBlocker.js
new file mode 100644
index 0000000000..7003f11bd8
--- /dev/null
+++ b/js/ui/screensaver/nameBlocker.js
@@ -0,0 +1,53 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const Gio = imports.gi.Gio;
+
+const BLOCKED_NAMES = [
+ 'org.gnome.ScreenSaver',
+ 'org.mate.ScreenSaver',
+];
+
+var NameBlocker = class NameBlocker {
+ constructor() {
+ this._watchIds = [];
+
+ for (let name of BLOCKED_NAMES) {
+ if (global.settings.get_boolean('debug-screensaver'))
+ global.log(`Screensaver blocker: Watching for name: '${name}'`);
+
+ let id = Gio.bus_watch_name(
+ Gio.BusType.SESSION,
+ name,
+ Gio.BusNameWatcherFlags.NONE,
+ (connection, busName, nameOwner) => this._onNameAppeared(connection, busName, nameOwner),
+ null
+ );
+ this._watchIds.push(id);
+ }
+ }
+
+ _onNameAppeared(connection, busName, nameOwner) {
+ if (global.settings.get_boolean('debug-screensaver'))
+ global.log(`Screensaver blocker: killing competing screensaver '${busName}' (owner: ${nameOwner})`);
+
+ connection.call(
+ busName,
+ '/' + busName.replace(/\./g, '/'),
+ busName,
+ 'Quit',
+ null,
+ null,
+ Gio.DBusCallFlags.NO_AUTO_START,
+ -1,
+ null,
+ null
+ );
+ }
+
+ destroy() {
+ for (let id of this._watchIds) {
+ Gio.bus_unwatch_name(id);
+ }
+ this._watchIds = [];
+ }
+};
diff --git a/js/ui/screensaver/notificationWidget.js b/js/ui/screensaver/notificationWidget.js
new file mode 100644
index 0000000000..e8ceb4a940
--- /dev/null
+++ b/js/ui/screensaver/notificationWidget.js
@@ -0,0 +1,102 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const GObject = imports.gi.GObject;
+const St = imports.gi.St;
+
+const Main = imports.ui.main;
+const MessageTray = imports.ui.messageTray;
+
+const ICON_SIZE_BASE = 24;
+
+var NotificationWidget = GObject.registerClass({
+ Signals: { 'count-changed': {} }
+}, class NotificationWidget extends St.BoxLayout {
+ _init() {
+ super._init({
+ style_class: 'notification-widget',
+ x_expand: false,
+ y_expand: false,
+ vertical: false
+ });
+
+ this._count = 0;
+ this._seenNotifications = new Set();
+ this._signalId = 0;
+
+ let iconSize = ICON_SIZE_BASE;
+
+ this._label = new St.Label({
+ style_class: 'notification-widget-label',
+ y_align: St.Align.MIDDLE
+ });
+ this.add_child(this._label);
+
+ this._icon = new St.Icon({
+ icon_name: 'xsi-notifications-symbolic',
+ icon_type: St.IconType.SYMBOLIC,
+ icon_size: iconSize,
+ style_class: 'notification-widget-icon',
+ y_align: St.Align.MIDDLE
+ });
+ this.add_child(this._icon);
+
+ this.hide();
+ }
+
+ activate() {
+ MessageTray.extensionsHandlingNotifications++;
+ this._signalId = Main.messageTray.connect(
+ 'notify-applet-update', this._onNotification.bind(this)
+ );
+ }
+
+ deactivate() {
+ if (this._signalId) {
+ Main.messageTray.disconnect(this._signalId);
+ this._signalId = 0;
+
+ MessageTray.extensionsHandlingNotifications =
+ Math.max(0, MessageTray.extensionsHandlingNotifications - 1);
+ }
+
+ this.reset();
+ }
+
+ reset() {
+ this._count = 0;
+ this._seenNotifications.clear();
+ this._updateDisplay();
+ }
+
+ _onNotification(tray, notification) {
+ if (notification.isTransient)
+ return;
+
+ if (this._seenNotifications.has(notification))
+ return;
+
+ this._seenNotifications.add(notification);
+ this._count++;
+ this._updateDisplay();
+ }
+
+ _updateDisplay() {
+ if (this._count > 0) {
+ this._label.text = this._count.toString();
+ this.show();
+ } else {
+ this.hide();
+ }
+
+ this.emit('count-changed');
+ }
+
+ shouldShow() {
+ return this._count > 0;
+ }
+
+ destroy() {
+ this.deactivate();
+ super.destroy();
+ }
+});
diff --git a/js/ui/screensaver/powerWidget.js b/js/ui/screensaver/powerWidget.js
new file mode 100644
index 0000000000..4fb33a5190
--- /dev/null
+++ b/js/ui/screensaver/powerWidget.js
@@ -0,0 +1,167 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const GObject = imports.gi.GObject;
+const St = imports.gi.St;
+const UPowerGlib = imports.gi.UPowerGlib;
+
+const PowerUtils = imports.misc.powerUtils;
+const SignalManager = imports.misc.signalManager;
+
+const ICON_SIZE_BASE = 24;
+const BATTERY_CRITICAL_PERCENT = 10;
+
+const {
+ UPDeviceKind,
+ UPDeviceState
+} = PowerUtils;
+
+var PowerWidget = GObject.registerClass({
+ Signals: { 'power-state-changed': {} }
+}, class PowerWidget extends St.BoxLayout {
+ _init() {
+ super._init({
+ style_class: 'power-widget',
+ x_expand: false,
+ y_expand: false,
+ vertical: false
+ });
+
+ this._iconSize = ICON_SIZE_BASE;
+ this._signalManager = new SignalManager.SignalManager(null);
+ this._client = null;
+ this._devices = [];
+ this._batteryCritical = false;
+
+ this._setupUPower();
+ }
+
+ _setupUPower() {
+ UPowerGlib.Client.new_async(null, (obj, res) => {
+ try {
+ this._client = UPowerGlib.Client.new_finish(res);
+ this._signalManager.connect(this._client, 'device-added', this._onDeviceChanged.bind(this));
+ this._signalManager.connect(this._client, 'device-removed', this._onDeviceChanged.bind(this));
+ this._updateDevices();
+ } catch (e) {
+ global.logError(`PowerWidget: Failed to connect to UPower: ${e.message}`);
+ this.hide();
+ }
+ });
+ }
+
+ _onDeviceChanged(client, device) {
+ this._updateDevices();
+ }
+
+ _updateDevices() {
+ if (!this._client)
+ return;
+
+ for (let device of this._devices) {
+ this._signalManager.disconnect('notify', device);
+ }
+ this._devices = [];
+
+ let devices = this._client.get_devices();
+ for (let device of devices) {
+ if (device.kind === UPDeviceKind.BATTERY || device.kind === UPDeviceKind.UPS) {
+ this._devices.push(device);
+ this._signalManager.connect(device, 'notify', this._onDevicePropertiesChanged.bind(this));
+ }
+ }
+
+ this._updateDisplay();
+ }
+
+ _onDevicePropertiesChanged(device, pspec) {
+ if (['percentage', 'state', 'icon-name'].includes(pspec.name)) {
+ this._updateDisplay();
+ }
+ }
+
+ _updateDisplay() {
+ this.destroy_all_children();
+ this._batteryCritical = false;
+
+ if (this._devices.length === 0 || !this._shouldShow()) {
+ this.hide();
+ this.emit('power-state-changed');
+ return;
+ }
+
+ for (let device of this._devices) {
+ let icon = this._createBatteryIcon(device);
+ this.add_child(icon);
+
+ if (device.percentage < BATTERY_CRITICAL_PERCENT) {
+ this._batteryCritical = true;
+ }
+ }
+
+ this.show();
+ this.emit('power-state-changed');
+ }
+
+ shouldShow() {
+ return this._devices.length > 0 && this._shouldShow();
+ }
+
+ _shouldShow() {
+ for (let device of this._devices) {
+ let state = device.state;
+
+ // Always show if discharging
+ if (state === UPDeviceState.DISCHARGING ||
+ state === UPDeviceState.PENDING_DISCHARGE) {
+ return true;
+ }
+
+ // Show if charging but not yet full
+ if (state === UPDeviceState.CHARGING ||
+ state === UPDeviceState.PENDING_CHARGE) {
+ return true;
+ }
+
+ // Show if critical, regardless of state
+ if (device.percentage < BATTERY_CRITICAL_PERCENT) {
+ return true;
+ }
+ }
+
+ // Don't show if fully charged and on AC
+ return false;
+ }
+
+ _createBatteryIcon(device) {
+ let iconName = PowerUtils.getBatteryIconName(device.percentage, device.state);
+
+ let icon = new St.Icon({
+ icon_name: iconName,
+ icon_type: St.IconType.SYMBOLIC,
+ icon_size: this._iconSize,
+ y_align: St.Align.MIDDLE,
+ style_class: 'power-widget-icon'
+ });
+
+ // Add critical styling if battery is low
+ if (device.percentage < BATTERY_CRITICAL_PERCENT) {
+ icon.add_style_class_name('power-widget-icon-critical');
+ }
+
+ return icon;
+ }
+
+ isBatteryCritical() {
+ return this._batteryCritical;
+ }
+
+ destroy() {
+ if (this._signalManager) {
+ this._signalManager.disconnectAllSignals();
+ }
+ this._client = null;
+ this._devices = [];
+
+ super.destroy();
+ }
+});
diff --git a/js/ui/screensaver/screenShield.js b/js/ui/screensaver/screenShield.js
new file mode 100644
index 0000000000..8abcef5a6a
--- /dev/null
+++ b/js/ui/screensaver/screenShield.js
@@ -0,0 +1,1229 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const Clutter = imports.gi.Clutter;
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const GObject = imports.gi.GObject;
+const Meta = imports.gi.Meta;
+const St = imports.gi.St;
+const Cinnamon = imports.gi.Cinnamon;
+
+const LoginManager = imports.misc.loginManager;
+const Util = imports.misc.util;
+const Main = imports.ui.main;
+const UnlockDialog = imports.ui.screensaver.unlockDialog;
+const ClockWidget = imports.ui.screensaver.clockWidget;
+const AlbumArtWidget = imports.ui.screensaver.albumArtWidget;
+const InfoPanel = imports.ui.screensaver.infoPanel;
+const NameBlocker = imports.ui.screensaver.nameBlocker;
+
+const SCREENSAVER_SCHEMA = 'org.cinnamon.desktop.screensaver';
+const POWER_SCHEMA = 'org.cinnamon.settings-daemon.plugins.power';
+const FADE_TIME = 200;
+const MOTION_THRESHOLD = 100;
+
+const FLOAT_TIMER_INTERVAL = 30;
+const DEBUG_FLOAT = false; // Set to true for 5-second intervals during development
+
+const MAX_SCREENSAVER_WIDGETS = 3;
+const WIDGET_LOAD_DELAY = 1000;
+
+var _debug = false;
+
+function _log(msg) {
+ if (_debug)
+ global.log(msg);
+}
+
+var _widgetRegistry = [];
+
+/**
+ * registerScreensaverWidget:
+ * @widgetClass: Widget class to register (must extend ScreensaverWidget)
+ *
+ * Register a screensaver widget. Extensions can use this to add custom widgets.
+ * Returns true if registered, false if registry is full or already registered.
+ */
+function registerScreensaverWidget(widgetClass) {
+ if (_widgetRegistry.length >= MAX_SCREENSAVER_WIDGETS) {
+ global.logWarning(`ScreenShield: Cannot register widget - registry full (max ${MAX_SCREENSAVER_WIDGETS})`);
+ return false;
+ }
+
+ if (_widgetRegistry.includes(widgetClass)) {
+ global.logWarning('ScreenShield: Widget class already registered');
+ return false;
+ }
+
+ _widgetRegistry.push(widgetClass);
+ _log(`ScreenShield: Registered widget class (total: ${_widgetRegistry.length})`);
+ return true;
+}
+
+/**
+ * deregisterScreensaverWidget:
+ * @widgetClass: Widget class to deregister
+ *
+ * Deregister a screensaver widget. Extensions can use this to remove/replace widgets.
+ * Returns true if deregistered, false if not found.
+ */
+function deregisterScreensaverWidget(widgetClass) {
+ let index = _widgetRegistry.indexOf(widgetClass);
+ if (index === -1) {
+ global.logWarning('ScreenShield: Widget class not found in registry');
+ return false;
+ }
+
+ _widgetRegistry.splice(index, 1);
+ _log(`ScreenShield: Deregistered widget class (total: ${_widgetRegistry.length})`);
+ return true;
+}
+
+const State = {
+ HIDDEN: 0, // Screensaver not active
+ SHOWN: 1, // Screensaver visible but not locked
+ LOCKED: 2, // Locked state (dialog hidden)
+ UNLOCKING: 3 // Unlock dialog visible
+};
+
+var ScreenShield = GObject.registerClass({
+ Signals: {
+ 'locked': {},
+ 'unlocked': {}
+ }
+}, class ScreenShield extends St.Widget {
+ _init() {
+ super._init({
+ name: 'screenShield',
+ style_class: 'screen-shield',
+ important: true,
+ visible: false,
+ reactive: true,
+ x: 0,
+ y: 0,
+ layout_manager: new Clutter.FixedLayout()
+ });
+
+ _debug = global.settings.get_boolean('debug-screensaver');
+
+ // Register stock widgets (only do this once, on first init)
+ if (_widgetRegistry.length === 0) {
+ registerScreensaverWidget(ClockWidget.ClockWidget);
+ registerScreensaverWidget(AlbumArtWidget.AlbumArtWidget);
+ }
+
+ this._state = State.HIDDEN;
+ this._lockTimeoutId = 0;
+ this._backgrounds = []; // Array of background actors, one per monitor
+ this._lastPointerMonitor = -1; // Track which monitor pointer is on
+ this._monitorsChangedId = 0;
+ this._widgets = []; // Array of screensaver widgets
+ this._awayMessage = null;
+ this._activationTime = 0;
+ this._floatTimerId = 0;
+ this._floatersNeedUpdate = false;
+ this._usedAwakePositions = new Set(); // Track used awake positions (as "halign:valign" keys)
+ this._usedAwakePositions.add(`${St.Align.MIDDLE}:${St.Align.MIDDLE}`); // Reserved for unlock dialog
+ this._usedFloatPositions = new Set(); // Track currently used float positions
+ this._widgetLoadTimeoutId = 0;
+ this._widgetLoadIdleId = 0;
+ this._infoPanel = null;
+ this._inhibitor = null;
+
+ this._nameBlocker = new NameBlocker.NameBlocker();
+
+ this._settings = new Gio.Settings({ schema_id: SCREENSAVER_SCHEMA });
+ this._settings.connect('changed::lock-enabled', this._syncInhibitor.bind(this));
+ this._powerSettings = new Gio.Settings({ schema_id: POWER_SCHEMA });
+ this._allowFloating = this._settings.get_boolean('floating-widgets');
+
+ let constraint = new Clutter.BindConstraint({
+ source: global.stage,
+ coordinate: Clutter.BindCoordinate.ALL
+ });
+ this.add_constraint(constraint);
+
+ Main.layoutManager.screenShieldGroup.add_actor(this);
+
+ this._backgroundLayer = new St.Widget({
+ name: 'screenShieldBackground',
+ style_class: 'screen-shield-background',
+ important: true,
+ reactive: false,
+ x_expand: true,
+ y_expand: true
+ });
+ this.add_child(this._backgroundLayer);
+
+ this._dialog = new UnlockDialog.UnlockDialog(this);
+ this.add_child(this._dialog);
+
+ this._keyboardBox = new St.Widget({
+ name: 'screensaverKeyboardBox',
+ layout_manager: new Clutter.BinLayout(),
+ visible: false,
+ reactive: true
+ });
+ this.add_child(this._keyboardBox);
+ this._oskVisible = false;
+
+ this._oskButton = new St.Button({
+ style_class: 'osk-activate-button',
+ important: true,
+ can_focus: true,
+ reactive: true
+ });
+ this._oskButton.set_child(new St.Icon({ icon_name: 'xsi-input-keyboard-symbolic' }));
+ this._oskButton.connect('clicked', this._toggleScreensaverKeyboard.bind(this));
+ this._keyboardBox.add_child(this._oskButton);
+
+ this._capturedEventId = 0;
+ this._lastMotionX = -1;
+ this._lastMotionY = -1;
+
+ this._loginManager = LoginManager.getLoginManager();
+ this._loginManager.connectPrepareForSleep(this._prepareForSleep.bind(this));
+ this._syncInhibitor();
+
+ this._loginManager.connect('lock', this._onSessionLock.bind(this));
+ this._loginManager.connect('unlock', this._onSessionUnlock.bind(this));
+ this._loginManager.connect('active', this._onSessionActive.bind(this));
+
+ this._monitorsChangedId = Main.layoutManager.connect('monitors-changed',
+ this._onMonitorsChanged.bind(this));
+
+ if (global.settings.get_boolean('session-locked-state')) {
+ _log('ScreenShield: Restoring locked state from previous session');
+ this._backupLockerCall('ReleaseGrabs', null, () => {
+ this.lock(true);
+ }, true);
+
+ }
+ }
+
+ _setState(newState) {
+ if (this._state === newState)
+ return;
+
+ const validTransitions = {
+ [State.HIDDEN]: [State.SHOWN, State.LOCKED],
+ [State.SHOWN]: [State.LOCKED, State.HIDDEN],
+ [State.LOCKED]: [State.UNLOCKING, State.HIDDEN],
+ [State.UNLOCKING]: [State.LOCKED, State.HIDDEN]
+ };
+
+ if (!validTransitions[this._state] || !validTransitions[this._state].includes(newState)) {
+ global.logError(`ScreenShield: Invalid state transition ${this._state} -> ${newState}`);
+ return;
+ }
+
+ let oldState = this._state;
+ this._state = newState;
+ _log(`ScreenShield: State ${oldState} -> ${newState}`);
+
+ let locked = newState === State.LOCKED || newState === State.UNLOCKING;
+ let wasLocked = oldState === State.LOCKED || oldState === State.UNLOCKING;
+ if (locked !== wasLocked) {
+ global.settings.set_boolean('session-locked-state', locked);
+ }
+
+ this._syncInhibitor();
+ }
+
+ /**
+ * _onCapturedEvent:
+ *
+ * Handle all input events via stage capture.
+ * This is connected to global.stage's captured-event signal when active.
+ */
+ _onCapturedEvent(actor, event) {
+ let type = event.type();
+
+ if (type !== Clutter.EventType.MOTION &&
+ type !== Clutter.EventType.BUTTON_PRESS &&
+ type !== Clutter.EventType.KEY_PRESS) {
+ return Clutter.EVENT_PROPAGATE;
+ }
+
+ if (type === Clutter.EventType.MOTION) {
+ this._updatePointerMonitor();
+
+ if (!this.isAwake() && !this._motionExceedsThreshold(event))
+ return Clutter.EVENT_PROPAGATE;
+ }
+
+ if (type === Clutter.EventType.KEY_PRESS) {
+ let symbol = event.get_key_symbol();
+
+ // Escape key cancels if not locked
+ if (symbol === Clutter.KEY_Escape && !this.isLocked()) {
+ this.deactivate();
+ return Clutter.EVENT_STOP;
+ }
+ }
+
+ // Wake up on user activity
+ let wasLocked = this._state === State.LOCKED;
+ let wasShown = this._state === State.SHOWN;
+ this.simulateUserActivity();
+
+ if (wasShown)
+ return Clutter.EVENT_STOP;
+
+ // Forward the initial keypress that woke the unlock dialog,
+ // before the entry has focus.
+ if (wasLocked && type === Clutter.EventType.KEY_PRESS) {
+ let unichar = event.get_key_unicode();
+ if (GLib.unichar_isprint(unichar)) {
+ this._dialog.addCharacter(unichar);
+ }
+ }
+
+ return Clutter.EVENT_PROPAGATE;
+ }
+
+ showUnlockDialog() {
+ if (this._state !== State.LOCKED)
+ return;
+
+ _log('ScreenShield: Showing unlock dialog');
+
+ this._clearClipboards();
+
+ this._setState(State.UNLOCKING);
+
+ this._lastPointerMonitor = global.display.get_current_monitor();
+ Main.setActionMode(this, Cinnamon.ActionMode.UNLOCK_SCREEN);
+
+ global.stage.show_cursor();
+
+ if (!this._dialog.initializePam()) {
+ global.logError('ScreenShield: PAM initialization failed, deactivating screensaver');
+ this._hideShield(false);
+ return;
+ }
+
+ this._dialog.opacity = 0;
+ this._dialog.show();
+
+ this._positionUnlockDialog();
+
+ this._keyboardBox.show();
+ this._positionKeyboardBox();
+
+ this._dialog.ease({
+ opacity: 255,
+ duration: FADE_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD
+ });
+
+ this._onWake();
+ }
+
+ hideUnlockDialog() {
+ if (this._state !== State.UNLOCKING)
+ return;
+
+ _log('ScreenShield: Hiding unlock dialog');
+
+ this._hideScreensaverKeyboard();
+ this._keyboardBox.hide();
+ this._clearClipboards();
+ this._lastMotionX = -1;
+ this._lastMotionY = -1;
+
+ this._dialog.ease({
+ opacity: 0,
+ duration: FADE_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => {
+ this._dialog.hide();
+
+ this._setState(State.LOCKED);
+
+ Main.setActionMode(this, Cinnamon.ActionMode.LOCK_SCREEN);
+
+ global.stage.hide_cursor();
+ this._onSleep();
+ }
+ });
+ }
+
+ _clearClipboards() {
+ let clipboard = St.Clipboard.get_default();
+ clipboard.set_text(St.ClipboardType.PRIMARY, '');
+ clipboard.set_text(St.ClipboardType.CLIPBOARD, '');
+ }
+
+ lock(immediate = false, awayMessage = null) {
+ if (this.isLocked())
+ return;
+
+ this._awayMessage = awayMessage;
+
+ _log(`ScreenShield: Locking screen (immediate=${immediate})`);
+
+ if (this._state === State.HIDDEN) {
+ this.activate(immediate);
+ }
+
+ this._stopLockDelay();
+ this._setLocked(true);
+ }
+
+ _setLocked(locked) {
+ if (locked === this.isLocked())
+ return;
+
+ if (locked) {
+ this._dialog.saveSystemLayout();
+ this._setState(State.LOCKED);
+ this.emit('locked');
+ } else {
+ this._setState(State.SHOWN);
+ }
+ }
+
+ unlock() {
+ if (!this.isLocked())
+ return;
+
+ _log('ScreenShield: Unlocking screen');
+
+ if (this._state === State.UNLOCKING) {
+ this._dialog.hide();
+ }
+
+ this._hideShield(true);
+ }
+
+ activate(immediate = false) {
+ if (this._state !== State.HIDDEN) {
+ _log('ScreenShield: Already active');
+ return;
+ }
+
+ _log(`ScreenShield: Activating screensaver (immediate=${immediate})`);
+
+ this._lastMotionX = -1;
+ this._lastMotionY = -1;
+ this._activationTime = GLib.get_monotonic_time();
+
+ if (!Main.pushModal(this, global.get_current_time(), 0, Cinnamon.ActionMode.LOCK_SCREEN)) {
+ global.logError('ScreenShield: Failed to acquire modal grab');
+ return;
+ }
+
+ this._setState(State.SHOWN);
+
+ this._createBackgrounds();
+
+ this._capturedEventId = global.stage.connect('captured-event',
+ this._onCapturedEvent.bind(this));
+
+ global.stage.hide_cursor();
+
+ if (Main.deskletContainer)
+ Main.deskletContainer.actor.hide();
+
+ Main.layoutManager.screenShieldGroup.show();
+ this.show();
+
+ if (immediate) {
+ this.opacity = 255;
+ this._activateBackupLocker();
+ this._scheduleWidgetLoading();
+ } else {
+ this.opacity = 0;
+ this.ease({
+ opacity: 255,
+ duration: FADE_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => {
+ this._activateBackupLocker();
+ this._scheduleWidgetLoading();
+ }
+ });
+ }
+
+ this._startLockDelay();
+ }
+
+ _startLockDelay() {
+ this._stopLockDelay();
+
+ if (!this._settings.get_boolean('lock-enabled'))
+ return;
+
+ let lockDelay = this._settings.get_uint('lock-delay');
+
+ if (lockDelay === 0) {
+ this._setLocked(true);
+ } else {
+ this._lockTimeoutId = GLib.timeout_add_seconds(
+ GLib.PRIORITY_DEFAULT,
+ lockDelay,
+ this._onLockDelayTimeout.bind(this)
+ );
+ }
+ }
+
+ _stopLockDelay() {
+ if (this._lockTimeoutId) {
+ GLib.source_remove(this._lockTimeoutId);
+ this._lockTimeoutId = 0;
+ }
+ }
+
+ _onLockDelayTimeout() {
+ this._lockTimeoutId = 0;
+ this._setLocked(true);
+ return GLib.SOURCE_REMOVE;
+ }
+
+ simulateUserActivity() {
+ if (this._state === State.LOCKED) {
+ this.showUnlockDialog();
+ } else if (this._state === State.SHOWN) {
+ this.deactivate();
+ }
+ }
+
+ deactivate() {
+ if (this.isLocked()) {
+ _log('ScreenShield: Cannot deactivate while locked');
+ return;
+ }
+
+ this._stopLockDelay();
+ this._hideShield(false);
+ }
+
+ _hideShield(emitUnlocked) {
+ this._hideScreensaverKeyboard();
+ this._keyboardBox.hide();
+ this._backupLockerCall('Unlock', null);
+
+ if (emitUnlocked)
+ this._dialog.restoreSystemLayout();
+
+ if (this._capturedEventId) {
+ global.stage.disconnect(this._capturedEventId);
+ this._capturedEventId = 0;
+ }
+
+ this.ease({
+ opacity: 0,
+ duration: FADE_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => {
+ Main.popModal(this);
+ this.hide();
+ Main.layoutManager.screenShieldGroup.hide();
+ this._destroyAllWidgets();
+ global.stage.show_cursor();
+
+ if (Main.deskletContainer)
+ Main.deskletContainer.actor.show();
+
+ this._activationTime = 0;
+ this._setState(State.HIDDEN);
+
+ if (emitUnlocked)
+ this.emit('unlocked');
+ }
+ });
+ }
+
+ isLocked() {
+ return this._state === State.LOCKED || this._state === State.UNLOCKING;
+ }
+
+ isAwake() {
+ return this._state === State.UNLOCKING;
+ }
+
+ getActiveTime() {
+ if (this._activationTime > 0)
+ return Math.floor((GLib.get_monotonic_time() - this._activationTime) / 1000000);
+ return 0;
+ }
+
+ _syncInhibitor() {
+ let lockEnabled = this._settings.get_boolean('lock-enabled');
+ let lockDisabled = Main.lockdownSettings.get_boolean('disable-lock-screen');
+ let shouldInhibit = this._state === State.HIDDEN && lockEnabled && !lockDisabled;
+
+ if (shouldInhibit && !this._inhibitor) {
+ _log('ScreenShield: Acquiring sleep inhibitor');
+ this._loginManager.inhibit('Cinnamon needs to lock the screen', (inhibitor) => {
+ if (!inhibitor) {
+ global.logWarning('ScreenShield: Failed to acquire sleep inhibitor');
+ return;
+ }
+
+ // Re-check after async - conditions may have changed
+ let stillNeeded = this._state === State.HIDDEN &&
+ this._settings.get_boolean('lock-enabled') &&
+ !Main.lockdownSettings.get_boolean('disable-lock-screen');
+ if (stillNeeded) {
+ this._inhibitor = inhibitor;
+ _log('ScreenShield: Sleep inhibitor acquired');
+ } else {
+ _log('ScreenShield: Sleep inhibitor no longer needed, releasing immediately');
+ inhibitor.close(null);
+ }
+ });
+ } else if (!shouldInhibit && this._inhibitor) {
+ _log('ScreenShield: Releasing sleep inhibitor');
+ this._inhibitor.close(null);
+ this._inhibitor = null;
+ }
+ }
+
+ _prepareForSleep(aboutToSuspend) {
+ if (aboutToSuspend) {
+ _log('ScreenShield: System suspending');
+
+ let lockOnSuspend = this._powerSettings.get_boolean('lock-on-suspend');
+ if (lockOnSuspend && !this.isLocked()) {
+ this.lock(true);
+ }
+ } else {
+ _log('ScreenShield: System resuming');
+
+ if (this._state === State.LOCKED) {
+ this.showUnlockDialog();
+ }
+ }
+ }
+
+ _onSessionLock() {
+ _log('ScreenShield: Received lock signal from LoginManager');
+ this.lock(true);
+ }
+
+ _onSessionUnlock() {
+ _log(`ScreenShield: Received unlock signal from LoginManager (state=${this._state}, isLocked=${this.isLocked()})`);
+ if (this.isLocked()) {
+ this.unlock();
+ } else if (this._state !== State.HIDDEN) {
+ this.deactivate();
+ }
+ }
+
+ _onSessionActive() {
+ _log(`ScreenShield: Received active signal from LoginManager (state=${this._state})`);
+ if (this._state === State.LOCKED) {
+ this.showUnlockDialog();
+ }
+ }
+
+ _motionExceedsThreshold(event) {
+ let [x, y] = event.get_coords();
+
+ if (this._lastMotionX < 0 || this._lastMotionY < 0) {
+ this._lastMotionX = x;
+ this._lastMotionY = y;
+ return false;
+ }
+
+ let distance = Math.max(Math.abs(this._lastMotionX - x),
+ Math.abs(this._lastMotionY - y));
+ return distance >= MOTION_THRESHOLD;
+ }
+
+ _updatePointerMonitor() {
+ let currentMonitor = global.display.get_current_monitor();
+
+ if (this._lastPointerMonitor !== currentMonitor) {
+ this._lastPointerMonitor = currentMonitor;
+
+ if (this._state === State.UNLOCKING && this._dialog.visible) {
+ this._positionUnlockDialog();
+ }
+
+ if (this._keyboardBox.visible)
+ this._positionKeyboardBox();
+
+ if (this.isAwake()) {
+ _log(`ScreenShield: Repositioning ${this._widgets.length} widgets to monitor ${currentMonitor}`);
+ for (let widget of this._widgets) {
+ widget.applyAwakePosition(currentMonitor);
+ this._positionWidgetByState(widget);
+ }
+ }
+
+ this._positionInfoPanel();
+ }
+ }
+
+ _positionUnlockDialog() {
+ if (!this._dialog)
+ return;
+
+ // Get current monitor geometry
+ let monitorIndex = global.display.get_current_monitor();
+ let monitor = Main.layoutManager.monitors[monitorIndex];
+
+ // Get dialog's preferred size
+ let [, natWidth] = this._dialog.get_preferred_width(-1);
+ let [, natHeight] = this._dialog.get_preferred_height(natWidth);
+
+ // When keyboard is visible, center dialog in the remaining space
+ let availableHeight = monitor.height;
+ let yOffset = monitor.y;
+ if (this._oskVisible) {
+ let size = Main.virtualKeyboardManager.getKeyboardSize();
+ let kbHeight = Math.floor(monitor.height / size);
+ let top = Main.virtualKeyboardManager.getKeyboardPosition() === 'top';
+
+ availableHeight = monitor.height - kbHeight;
+ if (top)
+ yOffset = monitor.y + kbHeight;
+ }
+
+ let x = monitor.x + (monitor.width - natWidth) / 2;
+ let y = yOffset + (availableHeight - natHeight) / 2;
+
+ this._dialog.set_position(x, y);
+ this._dialog.set_size(natWidth, natHeight);
+
+ _log(`ScreenShield: Positioned unlock dialog at ${x},${y} (${natWidth}x${natHeight}) on monitor ${monitorIndex}`);
+ }
+
+ _toggleScreensaverKeyboard() {
+ if (this._oskVisible)
+ this._hideScreensaverKeyboard();
+ else
+ this._showScreensaverKeyboard();
+ }
+
+ _showScreensaverKeyboard() {
+ if (this._oskVisible)
+ return;
+
+ this._oskButton.hide();
+ Main.virtualKeyboardManager.openForScreensaver(this._keyboardBox, this);
+ this._oskVisible = true;
+ this._positionKeyboardBox();
+ this._positionUnlockDialog();
+ }
+
+ _hideScreensaverKeyboard() {
+ if (!this._oskVisible)
+ return;
+
+ Main.virtualKeyboardManager.closeForScreensaver();
+ this._oskVisible = false;
+ this._oskButton.show();
+ this._positionKeyboardBox();
+ this._positionUnlockDialog();
+ }
+
+ _positionKeyboardBox() {
+ let monitorIndex = global.display.get_current_monitor();
+ let monitor = Main.layoutManager.monitors[monitorIndex];
+
+ if (this._oskVisible) {
+ let size = Main.virtualKeyboardManager.getKeyboardSize();
+ let top = Main.virtualKeyboardManager.getKeyboardPosition() === 'top';
+
+ let height = Math.floor(monitor.height / size);
+ let y = top ? monitor.y : monitor.y + monitor.height - height;
+
+ this._keyboardBox.set_position(monitor.x, y);
+ this._keyboardBox.set_size(monitor.width, height);
+
+ let keyboard = Main.virtualKeyboardManager.keyboardActor;
+ if (keyboard) {
+ keyboard.width = monitor.width;
+ keyboard.height = height;
+ }
+ } else {
+ let [, natWidth] = this._oskButton.get_preferred_width(-1);
+ let [, natHeight] = this._oskButton.get_preferred_height(natWidth);
+ let padding = 24 * global.ui_scale;
+
+ let x = monitor.x + (monitor.width - natWidth) / 2;
+ let y = monitor.y + monitor.height - natHeight - padding;
+
+ this._keyboardBox.set_position(Math.floor(x), Math.floor(y));
+ this._keyboardBox.set_size(natWidth, natHeight);
+ }
+ }
+
+ _onMonitorsChanged() {
+ if (this._state === State.HIDDEN)
+ return;
+
+ _log('ScreenShield: Monitors changed, updating backgrounds and layout');
+
+ this._createBackgrounds();
+
+ this._positionInfoPanel();
+
+ if (this._keyboardBox.visible)
+ this._positionKeyboardBox();
+
+ if (this._state === State.UNLOCKING && this._dialog.visible) {
+ this._lastPointerMonitor = -1;
+ this._updatePointerMonitor();
+ }
+ }
+
+ _positionWidget(widget, monitor, position) {
+ widget._isBeingPositioned = true;
+
+ // Divide monitor into 3x3 grid
+ let sectorWidth = monitor.width / 3;
+ let sectorHeight = monitor.height / 3;
+
+ // St.Align values map directly to sector indices (START=0, MIDDLE=1, END=2)
+ let sectorX = position.halign;
+ let sectorY = position.valign;
+
+ // Calculate sector bounds
+ let sectorLeft = monitor.x + (sectorX * sectorWidth);
+ let sectorTop = monitor.y + (sectorY * sectorHeight);
+
+ // Get widget's preferred size
+ let [, natWidth] = widget.get_preferred_width(-1);
+ let [, natHeight] = widget.get_preferred_height(natWidth);
+
+ // Constrain widget size to fit within sector
+ let widgetWidth = Math.min(natWidth, sectorWidth);
+ let widgetHeight = Math.min(natHeight, sectorHeight);
+
+ // If we constrained width, recalculate height with new width
+ if (widgetWidth < natWidth) {
+ [, natHeight] = widget.get_preferred_height(widgetWidth);
+ widgetHeight = Math.min(natHeight, sectorHeight);
+ }
+
+ let x = sectorLeft + (sectorWidth - widgetWidth) / 2;
+ let y = sectorTop + (sectorHeight - widgetHeight) / 2;
+
+ widget.set_position(Math.floor(x), Math.floor(y));
+ widget._isBeingPositioned = false;
+ }
+
+ _scheduleWidgetLoading() {
+ this._cancelWidgetLoading();
+
+ this._widgetLoadTimeoutId = GLib.timeout_add(
+ GLib.PRIORITY_DEFAULT,
+ WIDGET_LOAD_DELAY,
+ () => {
+ this._widgetLoadTimeoutId = 0;
+ this._startLoadingWidgets();
+ return GLib.SOURCE_REMOVE;
+ }
+ );
+ }
+
+ _startLoadingWidgets() {
+ this._createInfoPanel();
+
+ if (_widgetRegistry.length === 0) {
+ _log('ScreenShield: No widgets to load');
+ return;
+ }
+
+ let widgetIndex = 0;
+
+ this._widgetLoadIdleId = GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => {
+ if (widgetIndex >= _widgetRegistry.length) {
+ this._widgetLoadIdleId = 0;
+ return GLib.SOURCE_REMOVE;
+ }
+
+ try {
+ let widgetClass = _widgetRegistry[widgetIndex];
+ let widget = new widgetClass(this._awayMessage);
+ this._addScreenShieldWidget(widget);
+ } catch (e) {
+ global.logError(`ScreenShield: Failed to load widget ${widgetIndex}: ${e.message}`);
+ }
+
+ widgetIndex++;
+ return GLib.SOURCE_CONTINUE;
+ });
+ }
+
+ _cancelWidgetLoading() {
+ if (this._widgetLoadTimeoutId) {
+ GLib.source_remove(this._widgetLoadTimeoutId);
+ this._widgetLoadTimeoutId = 0;
+ }
+
+ if (this._widgetLoadIdleId) {
+ GLib.source_remove(this._widgetLoadIdleId);
+ this._widgetLoadIdleId = 0;
+ }
+ }
+
+ _addScreenShieldWidget(widget) {
+ this._validateAwakePosition(widget);
+
+ if (this.isAwake() || !this._allowFloating) {
+ let currentMonitor = global.display.get_current_monitor();
+ widget.applyAwakePosition(currentMonitor);
+ if (this.isAwake()) {
+ widget.onAwake();
+ }
+ } else {
+ this._assignRandomPositionToWidget(widget);
+ widget.applyNextPosition();
+
+ if (this._widgets.length === 0) {
+ this._startFloatTimer();
+ }
+ }
+
+ widget._allocationChangedId = widget.connect('allocation-changed',
+ this._onWidgetAllocationChanged.bind(this, widget));
+
+ this._widgets.push(widget);
+
+ this.add_child(widget);
+
+ widget.onScreensaverActivated();
+ }
+
+ _onWidgetAllocationChanged(widget) {
+ if (widget._isBeingPositioned)
+ return;
+
+ this._positionWidgetByState(widget);
+ }
+
+ _validateAwakePosition(widget) {
+ let pos = widget.getAwakePosition();
+ let posKey = `${pos.halign}:${pos.valign}`;
+
+ if (this._usedAwakePositions.has(posKey)) {
+ let newPos = this._findAvailableAwakePosition();
+ if (newPos) {
+ global.logWarning(`ScreenShield: Widget awake position ${posKey} conflicts, ` +
+ `reassigning to ${newPos.halign}:${newPos.valign}`);
+ widget.setAwakePosition(0, newPos.halign, newPos.valign);
+ posKey = `${newPos.halign}:${newPos.valign}`;
+ } else {
+ global.logWarning(`ScreenShield: No available awake position for widget, ` +
+ `using conflicting position ${posKey}`);
+ }
+ }
+
+ this._usedAwakePositions.add(posKey);
+ }
+
+ _findAvailableAwakePosition() {
+ let alignments = [St.Align.START, St.Align.MIDDLE, St.Align.END];
+
+ for (let halign of alignments) {
+ for (let valign of alignments) {
+ let posKey = `${halign}:${valign}`;
+ if (!this._usedAwakePositions.has(posKey)) {
+ return { halign, valign };
+ }
+ }
+ }
+
+ return null;
+ }
+
+ _destroyAllWidgets() {
+ this._cancelWidgetLoading();
+
+ this._stopFloatTimer();
+ this._destroyInfoPanel();
+ for (let widget of this._widgets) {
+ if (widget._allocationChangedId) {
+ widget.disconnect(widget._allocationChangedId);
+ widget._allocationChangedId = 0;
+ }
+ widget.onScreensaverDeactivated();
+ widget.destroy();
+ }
+
+ this._widgets = [];
+
+ this._usedAwakePositions.clear();
+ this._usedAwakePositions.add(`${St.Align.MIDDLE}:${St.Align.MIDDLE}`); // Reserved for unlock dialog
+ this._usedFloatPositions.clear();
+ }
+
+ _createInfoPanel() {
+ if (this._infoPanel)
+ return;
+
+ this._infoPanel = new InfoPanel.InfoPanel();
+ this._infoPanel.connect('allocation-changed', this._positionInfoPanel.bind(this));
+ this.add_child(this._infoPanel);
+ this._infoPanel.onScreensaverActivated();
+ }
+
+ _destroyInfoPanel() {
+ if (this._infoPanel) {
+ this._infoPanel.onScreensaverDeactivated();
+ this._infoPanel.destroy();
+ this._infoPanel = null;
+ }
+ }
+
+ _positionInfoPanel() {
+ if (!this._infoPanel)
+ return;
+
+ let currentMonitor = global.display.get_current_monitor();
+ let monitor = Main.layoutManager.monitors[currentMonitor];
+ if (!monitor)
+ monitor = Main.layoutManager.primaryMonitor;
+
+ let [, natWidth] = this._infoPanel.get_preferred_width(-1);
+ let [, natHeight] = this._infoPanel.get_preferred_height(natWidth);
+
+ let padding = 12 * global.ui_scale;
+ let x = monitor.x + monitor.width - natWidth - padding;
+ let y = monitor.y + padding;
+
+ this._infoPanel.set_position(Math.floor(x), Math.floor(y));
+ }
+
+ _positionWidgetByState(widget) {
+ let pos = widget.getCurrentPosition();
+ let monitor = Main.layoutManager.monitors[pos.monitor];
+
+ if (!monitor) {
+ monitor = Main.layoutManager.primaryMonitor;
+ }
+
+ this._positionWidget(widget, monitor, pos);
+ }
+
+ _startFloatTimer() {
+ this._stopFloatTimer();
+
+ // Don't start if floating is disabled or no floating widgets
+ if (!this._allowFloating || this._widgets.length === 0)
+ return;
+
+ let interval = DEBUG_FLOAT ? 5 : FLOAT_TIMER_INTERVAL;
+
+ this._floatTimerId = GLib.timeout_add_seconds(
+ GLib.PRIORITY_DEFAULT,
+ interval,
+ this._onFloatTimer.bind(this)
+ );
+
+ _log(`ScreenShield: Started float timer (${interval} seconds)`);
+ }
+
+ _stopFloatTimer() {
+ if (this._floatTimerId) {
+ GLib.source_remove(this._floatTimerId);
+ this._floatTimerId = 0;
+ }
+ }
+
+ _onFloatTimer() {
+ this._floatersNeedUpdate = true;
+ this._updateFloaters();
+ return GLib.SOURCE_CONTINUE;
+ }
+
+ _updateFloaters() {
+ if (!this._floatersNeedUpdate || this._widgets.length === 0)
+ return;
+
+ if (this.isAwake() || !this._allowFloating) {
+ this._floatersNeedUpdate = false;
+ return;
+ }
+
+ this._assignRandomPositions();
+
+ for (let widget of this._widgets) {
+ widget.applyNextPosition();
+ this._positionWidgetByState(widget);
+ }
+
+ this._floatersNeedUpdate = false;
+ }
+
+ _assignRandomPositionToWidget(widget) {
+ // Build list of available positions (excluding already used)
+ let availablePositions = [];
+ let nMonitors = Main.layoutManager.monitors.length;
+ let alignments = [St.Align.START, St.Align.MIDDLE, St.Align.END];
+
+ for (let monitor = 0; monitor < nMonitors; monitor++) {
+ for (let halign of alignments) {
+ for (let valign of alignments) {
+ let posKey = `${monitor}:${halign}:${valign}`;
+ if (!this._usedFloatPositions.has(posKey)) {
+ availablePositions.push({ monitor, halign, valign, key: posKey });
+ }
+ }
+ }
+ }
+
+ if (availablePositions.length === 0) {
+ // All positions used, just pick a random one
+ let monitor = Math.floor(Math.random() * nMonitors);
+ let halign = alignments[Math.floor(Math.random() * 3)];
+ let valign = alignments[Math.floor(Math.random() * 3)];
+ widget.setNextPosition(monitor, halign, valign);
+ return;
+ }
+
+ let idx = Math.floor(Math.random() * availablePositions.length);
+ let pos = availablePositions[idx];
+ widget.setNextPosition(pos.monitor, pos.halign, pos.valign);
+ this._usedFloatPositions.add(pos.key);
+ }
+
+ _assignRandomPositions() {
+ this._usedFloatPositions.clear();
+
+ for (let widget of this._widgets) {
+ this._assignRandomPositionToWidget(widget);
+ }
+ }
+
+ _onWake() {
+ _log('ScreenShield: Waking up');
+
+ this._stopFloatTimer();
+
+ let currentMonitor = global.display.get_current_monitor();
+ for (let widget of this._widgets) {
+ widget.applyAwakePosition(currentMonitor);
+ widget.onAwake();
+ this._positionWidgetByState(widget);
+ }
+
+ if (this._infoPanel)
+ this._infoPanel.onWake();
+ }
+
+ _onSleep() {
+ _log('ScreenShield: Going to sleep');
+
+ for (let widget of this._widgets) {
+ widget.onSleep();
+ }
+
+ if (this._infoPanel)
+ this._infoPanel.onSleep();
+
+ this._startFloatTimer();
+ this._floatersNeedUpdate = true;
+ this._updateFloaters();
+ }
+
+ _activateBackupLocker() {
+ let stageXid = global.get_stage_xwindow();
+ let [termTty, sessionTty] = Util.getTtyVals();
+ this._backupLockerCall('Lock',
+ GLib.Variant.new('(tuu)', [stageXid, termTty, sessionTty]));
+ }
+
+ _backupLockerCall(method, params, callback, noAutoStart = false) {
+ Gio.DBus.session.call(
+ 'org.cinnamon.BackupLocker',
+ '/org/cinnamon/BackupLocker',
+ 'org.cinnamon.BackupLocker',
+ method,
+ params,
+ null,
+ noAutoStart ? Gio.DBusCallFlags.NO_AUTO_START : Gio.DBusCallFlags.NONE,
+ 5000,
+ null,
+ (connection, result) => {
+ try {
+ connection.call_finish(result);
+ } catch (e) {
+ global.logWarning(`ScreenShield: BackupLocker.${method} failed: ${e.message}`);
+ }
+ if (callback)
+ callback();
+ }
+ );
+ }
+
+ _createBackgrounds() {
+ this._destroyBackgrounds();
+
+ if (Meta.is_wayland_compositor()) {
+ // TODO: Waiting on:
+ // muffin: https://github.com/linuxmint/muffin/pull/784
+ // cinnamon-settings-daemon: https://github.com/linuxmint/cinnamon-settings-daemon/pull/437
+ //
+ // Once those are merged we can access the layer-shell surfaces of csd-background and avoid
+ // having to load them in Cinnamon.
+ //
+ // For now, there is only a black background for the screensaver.
+ return;
+ }
+
+ let nMonitors = Main.layoutManager.monitors.length;
+
+ for (let i = 0; i < nMonitors; i++) {
+ let monitor = Main.layoutManager.monitors[i];
+ let background = Meta.X11BackgroundActor.new_for_display(global.display);
+
+ background.reactive = false;
+
+ background.set_position(monitor.x, monitor.y);
+ background.set_size(monitor.width, monitor.height);
+
+ let effect = new Clutter.BrightnessContrastEffect();
+ effect.set_brightness(-0.7); // Darken by 70%
+ background.add_effect(effect);
+
+ this._backgroundLayer.add_child(background);
+
+ this._backgrounds.push(background);
+ }
+ }
+
+ _destroyBackgrounds() {
+ for (let bg of this._backgrounds) {
+ bg.destroy();
+ }
+ this._backgrounds = [];
+ }
+
+ vfunc_destroy() {
+ this._stopLockDelay();
+ this._cancelWidgetLoading();
+ this._backupLockerCall('Unlock', null);
+
+ if (this._inhibitor) {
+ this._inhibitor.close(null);
+ this._inhibitor = null;
+ }
+
+ if (this._capturedEventId) {
+ global.stage.disconnect(this._capturedEventId);
+ this._capturedEventId = 0;
+ }
+
+ if (this._monitorsChangedId) {
+ Main.layoutManager.disconnect(this._monitorsChangedId);
+ this._monitorsChangedId = 0;
+ }
+
+ this._destroyBackgrounds();
+ this._destroyAllWidgets();
+
+ if (this._nameBlocker) {
+ this._nameBlocker.destroy();
+ this._nameBlocker = null;
+ }
+
+ super.vfunc_destroy();
+ }
+});
diff --git a/js/ui/screensaver/screensaverWidget.js b/js/ui/screensaver/screensaverWidget.js
new file mode 100644
index 0000000000..870e458654
--- /dev/null
+++ b/js/ui/screensaver/screensaverWidget.js
@@ -0,0 +1,90 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const GObject = imports.gi.GObject;
+const St = imports.gi.St;
+
+/**
+ * FloatPosition:
+ * Structure representing a position in the screensaver's 3x3 grid system.
+ * Uses St.Align values (START, MIDDLE, END) for alignment.
+ * Used for current, awake, and next positions of floating widgets.
+ */
+var FloatPosition = class FloatPosition {
+ constructor(monitor = 0, halign = St.Align.MIDDLE, valign = St.Align.MIDDLE) {
+ this.monitor = monitor;
+ this.halign = halign;
+ this.valign = valign;
+ }
+
+ copyFrom(other) {
+ this.monitor = other.monitor;
+ this.halign = other.halign;
+ this.valign = other.valign;
+ }
+
+};
+
+/**
+ * ScreensaverWidget:
+ *
+ * Base class for screensaver widgets that float on the lock screen.
+ * All ScreensaverWidgets participate in the floating system - they are
+ * randomly repositioned periodically in a 3x3 grid per monitor.
+ *
+ * When the unlock dialog is visible ("awake"), widgets move to
+ * their designated awake positions.
+ *
+ * Non-floating widgets (like PowerWidget) should not subclass this.
+ */
+var ScreensaverWidget = GObject.registerClass(
+class ScreensaverWidget extends St.BoxLayout {
+ _init(params) {
+ super._init(params);
+
+ this._currentPosition = new FloatPosition();
+ this._awakePosition = new FloatPosition();
+ this._nextPosition = new FloatPosition();
+ }
+
+ setAwakePosition(monitor, halign, valign) {
+ this._awakePosition.monitor = monitor;
+ this._awakePosition.halign = halign;
+ this._awakePosition.valign = valign;
+ }
+
+ setNextPosition(monitor, halign, valign) {
+ this._nextPosition.monitor = monitor;
+ this._nextPosition.halign = halign;
+ this._nextPosition.valign = valign;
+ }
+
+ applyNextPosition() {
+ this._currentPosition.copyFrom(this._nextPosition);
+ }
+
+ applyAwakePosition(currentMonitor) {
+ this._awakePosition.monitor = currentMonitor;
+ this._nextPosition.copyFrom(this._awakePosition);
+ this.applyNextPosition();
+ }
+
+ getCurrentPosition() {
+ return this._currentPosition;
+ }
+
+ getAwakePosition() {
+ return this._awakePosition;
+ }
+
+ onScreensaverActivated() {
+ }
+
+ onScreensaverDeactivated() {
+ }
+
+ onAwake() {
+ }
+
+ onSleep() {
+ }
+});
diff --git a/js/ui/screensaver/unlockDialog.js b/js/ui/screensaver/unlockDialog.js
new file mode 100644
index 0000000000..100a0fb756
--- /dev/null
+++ b/js/ui/screensaver/unlockDialog.js
@@ -0,0 +1,430 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const AccountsService = imports.gi.AccountsService;
+const Clutter = imports.gi.Clutter;
+const GLib = imports.gi.GLib;
+const GObject = imports.gi.GObject;
+const Gio = imports.gi.Gio;
+const Meta = imports.gi.Meta;
+const St = imports.gi.St;
+const Pango = imports.gi.Pango;
+
+const AuthClient = imports.misc.authClient;
+const CinnamonEntry = imports.ui.cinnamonEntry;
+const KeyboardManager = imports.ui.keyboardManager;
+const UserWidget = imports.ui.userWidget;
+const Util = imports.misc.util;
+const Main = imports.ui.main;
+
+const IDLE_TIMEOUT = 30; // seconds - hide dialog after this much idle time
+const DEBUG_IDLE = false; // Set to true for 5-second timeout during development
+
+var UnlockDialog = GObject.registerClass(
+class UnlockDialog extends St.BoxLayout {
+ _init(screenShield) {
+ super._init({
+ vertical: true,
+ reactive: true,
+ visible: false,
+ x_align: Clutter.ActorAlign.CENTER,
+ y_align: Clutter.ActorAlign.CENTER,
+ x_expand: true,
+ y_expand: true
+ });
+
+ this._screenShield = screenShield;
+ this._idleMonitor = Meta.IdleMonitor.get_core();
+ this._idleWatchId = 0;
+
+ this._dialogBox = new St.BoxLayout({
+ style_class: 'dialog prompt-dialog',
+ important: true,
+ vertical: true,
+ x_align: Clutter.ActorAlign.CENTER,
+ y_align: Clutter.ActorAlign.CENTER
+ });
+ this.add_child(this._dialogBox);
+
+ this._contentLayout = new St.BoxLayout({
+ style_class: 'dialog-content-box',
+ important: true,
+ vertical: true
+ });
+ this._dialogBox.add_child(this._contentLayout);
+
+ let username = GLib.get_user_name();
+ this._userManager = AccountsService.UserManager.get_default();
+ this._user = this._userManager.get_user(username);
+
+ this._userWidget = new UserWidget.UserWidget(this._user, Clutter.Orientation.VERTICAL);
+ this._userWidget.x_align = Clutter.ActorAlign.CENTER;
+ this._contentLayout.add_child(this._userWidget);
+
+ let passwordBox = new St.BoxLayout({
+ style_class: 'prompt-dialog-password-layout',
+ important: true,
+ vertical: true
+ });
+ this._contentLayout.add_child(passwordBox);
+
+ this._passwordEntry = new St.PasswordEntry({
+ style_class: 'prompt-dialog-password-entry',
+ important: true,
+ hint_text: _("Password"),
+ can_focus: true,
+ x_align: Clutter.ActorAlign.CENTER
+ });
+ passwordBox.add_child(this._passwordEntry);
+
+ this._capsLockWarning = new CinnamonEntry.CapsLockWarning();
+ this._capsLockWarning.x_align = Clutter.ActorAlign.CENTER;
+ passwordBox.add_child(this._capsLockWarning);
+
+ // Info label (for auth-info messages from PAM)
+ this._infoLabel = new St.Label({
+ style_class: 'prompt-dialog-info-label',
+ important: true,
+ text: '',
+ x_align: Clutter.ActorAlign.CENTER
+ });
+ this._infoLabel.clutter_text.line_wrap = true;
+ this._infoLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+ passwordBox.add_child(this._infoLabel);
+
+ // Message label (for errors)
+ this._messageLabel = new St.Label({
+ style_class: 'prompt-dialog-error-label',
+ important: true,
+ text: '',
+ x_align: Clutter.ActorAlign.CENTER
+ });
+ this._messageLabel.clutter_text.line_wrap = true;
+ this._messageLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+ passwordBox.add_child(this._messageLabel);
+
+ this._sourceChangedId = 0;
+ this._inputSourceManager = KeyboardManager.getInputSourceManager();
+ this._systemSourceIndex = null;
+
+ if (this._inputSourceManager.multipleSources) {
+ this._updateLayoutIndicator();
+ this._passwordEntry.connect('primary-icon-clicked', () => {
+ let currentIndex = this._inputSourceManager.currentSource.index;
+ let nextIndex = (currentIndex + 1) % this._inputSourceManager.numInputSources;
+ this._inputSourceManager.activateInputSourceIndex(nextIndex);
+ });
+
+ this._sourceChangedId = this._inputSourceManager.connect(
+ 'current-source-changed', this._updateLayoutIndicator.bind(this));
+ }
+
+ this._buttonLayout = new St.Widget({
+ style_class: 'dialog-button-box',
+ important: true,
+ layout_manager: new Clutter.BoxLayout({
+ homogeneous: true,
+ spacing: 12 * global.ui_scale
+ })
+ });
+ this._dialogBox.add(this._buttonLayout, {
+ x_align: St.Align.MIDDLE,
+ y_align: St.Align.MIDDLE
+ });
+
+ this._cancelButton = new St.Button({
+ style_class: 'dialog-button',
+ important: true,
+ label: _("Cancel"),
+ reactive: true,
+ can_focus: true,
+ x_expand: true,
+ y_expand: true
+ });
+ this._cancelButton.connect('clicked', this._onCancel.bind(this));
+ this._buttonLayout.add_child(this._cancelButton);
+
+ this._screensaverSettings = new Gio.Settings({ schema_id: 'org.cinnamon.desktop.screensaver' });
+ if (this._screensaverSettings.get_boolean('user-switch-enabled') &&
+ !Main.lockdownSettings.get_boolean('disable-user-switching')) {
+ this._switchUserButton = new St.Button({
+ style_class: 'dialog-button',
+ important: true,
+ label: _("Switch User"),
+ can_focus: true,
+ reactive: true,
+ x_expand: true,
+ y_expand: true
+ });
+ this._switchUserButton.connect('clicked', this._onSwitchUser.bind(this));
+ this._buttonLayout.add_child(this._switchUserButton);
+ }
+
+ this._unlockButton = new St.Button({
+ style_class: 'dialog-button',
+ important: true,
+ label: _("Unlock"),
+ can_focus: true,
+ reactive: false,
+ x_expand: true,
+ y_expand: true
+ });
+ this._unlockButton.add_style_pseudo_class('default');
+ this._unlockButton.connect('clicked', this._onUnlock.bind(this));
+ this._buttonLayout.add_child(this._unlockButton);
+
+ this._passwordEntry.clutter_text.connect('text-changed', text => {
+ this._unlockButton.reactive = text.get_text().length > 0;
+ });
+
+ this._authClient = new AuthClient.AuthClient();
+ this._authClient.connect('auth-success', this._onAuthSuccess.bind(this));
+ this._authClient.connect('auth-failure', this._onAuthFailure.bind(this));
+ this._authClient.connect('auth-cancel', this._onAuthCancel.bind(this));
+ this._authClient.connect('auth-busy', this._onAuthBusy.bind(this));
+ this._authClient.connect('auth-prompt', this._onAuthPrompt.bind(this));
+ this._authClient.connect('auth-info', this._onAuthInfo.bind(this));
+ this._authClient.connect('auth-error', this._onAuthError.bind(this));
+
+ this._passwordEntry.clutter_text.connect('activate', this._onUnlock.bind(this));
+ this.connect('key-press-event', this._onKeyPress.bind(this));
+ }
+
+ saveSystemLayout() {
+ if (!this._inputSourceManager.multipleSources)
+ return;
+
+ let currentSource = this._inputSourceManager.currentSource;
+ if (currentSource)
+ this._systemSourceIndex = currentSource.index;
+ }
+
+ _applyLockscreenLayout() {
+ if (!this._inputSourceManager.multipleSources)
+ return;
+
+ let savedIndex = this._screensaverSettings.get_int('layout-group');
+
+ if (savedIndex < 0) {
+ savedIndex = this._inputSourceManager.currentSource.index;
+ this._screensaverSettings.set_int('layout-group', savedIndex);
+ }
+
+ if (savedIndex !== this._inputSourceManager.currentSource.index)
+ this._inputSourceManager.activateInputSourceIndex(savedIndex);
+ }
+
+ _saveLockscreenLayout() {
+ if (!this._inputSourceManager.multipleSources)
+ return;
+
+ let currentSource = this._inputSourceManager.currentSource;
+ if (currentSource)
+ this._screensaverSettings.set_int('layout-group', currentSource.index);
+ }
+
+ restoreSystemLayout() {
+ if (!this._inputSourceManager.multipleSources)
+ return;
+
+ this._saveLockscreenLayout();
+
+ if (this._systemSourceIndex !== null &&
+ this._systemSourceIndex !== this._inputSourceManager.currentSource.index) {
+ this._inputSourceManager.activateInputSourceIndex(this._systemSourceIndex);
+ }
+
+ this._systemSourceIndex = null;
+ }
+
+ _updateLayoutIndicator() {
+ let source = this._inputSourceManager.currentSource;
+ if (!source)
+ return;
+
+ let icon = null;
+
+ if (this._inputSourceManager.showFlags)
+ icon = this._inputSourceManager.createFlagIcon(source, null, 16);
+
+ if (!icon)
+ icon = new St.Label({ text: source.shortName });
+
+ this._passwordEntry.set_primary_icon(icon);
+ }
+
+ _onKeyPress(actor, event) {
+ let symbol = event.get_key_symbol();
+ if (symbol === Clutter.KEY_Escape) {
+ this._onCancel();
+ return Clutter.EVENT_STOP;
+ }
+ return Clutter.EVENT_PROPAGATE;
+ }
+
+ _onUnlock() {
+ let password = this._passwordEntry.get_text();
+
+ if (password.length == 0)
+ return;
+
+ this._authClient.sendPassword(password);
+ this._passwordEntry.set_text('');
+ }
+
+ _onAuthPrompt(authClient, prompt) {
+ let hintText;
+ if (prompt.toLowerCase().includes('password:')) {
+ hintText = _("Please enter your password...");
+ } else {
+ hintText = prompt.replace(/:$/, '');
+ }
+ if (global.settings.get_boolean('debug-screensaver'))
+ global.log(`UnlockDialog: prompt='${prompt}', hintText='${hintText}'`);
+
+ this._infoLabel.text = '';
+ this._passwordEntry.hint_text = hintText;
+ this._setPasswordEntryVisible(true);
+ global.stage.set_key_focus(this._passwordEntry);
+ }
+
+ _onAuthInfo(authClient, info) {
+ this._infoLabel.text = info;
+ }
+
+ _onAuthError(authClient, error) {
+ this._messageLabel.text = error;
+ }
+
+ _onAuthSuccess() {
+ this._setBusy(false);
+ this._messageLabel.text = '';
+ this._infoLabel.text = '';
+ this._screenShield.unlock();
+ }
+
+ _onAuthFailure() {
+ this._setBusy(false);
+
+ this._infoLabel.text = '';
+ this._passwordEntry.set_text('');
+ this._setPasswordEntryVisible(false);
+ Util.wiggle(this._dialogBox);
+ }
+
+ _onAuthCancel() {
+ this._setBusy(false);
+ this._messageLabel.text = '';
+ this._infoLabel.text = '';
+ this.initializePam();
+ }
+
+ _onAuthBusy(authClient, busy) {
+ this._setBusy(busy);
+ }
+
+ _setBusy(busy) {
+ if (busy) {
+ this._messageLabel.text = '';
+ this._infoLabel.text = '';
+
+ this._passwordEntry.reactive = false;
+ this._passwordEntry.hint_text = _("Checking...");
+ } else {
+ this._passwordEntry.reactive = true;
+ }
+ }
+
+ _onCancel() {
+ if (this._authClient && this._authClient.initialized) {
+ this._authClient.cancel();
+ }
+
+ this._screenShield.hideUnlockDialog();
+ }
+
+ _onSwitchUser() {
+ Util.switchToGreeter();
+ }
+
+ initializePam() {
+ if (!this._authClient.initialized)
+ return this._authClient.initialize();
+
+ return true;
+ }
+
+ _setPasswordEntryVisible(visible) {
+ if (visible) {
+ this._passwordEntry.show();
+ this._unlockButton.show();
+ this._capsLockWarning.show();
+ } else {
+ this._passwordEntry.hide();
+ this._unlockButton.hide();
+ this._capsLockWarning.hide();
+ }
+ }
+
+ show() {
+ this._passwordEntry.text = '';
+ this._messageLabel.text = '';
+ this._infoLabel.text = '';
+ this._passwordEntry.reactive = true;
+ this._passwordEntry.hint_text = _("Password");
+
+ this._setPasswordEntryVisible(false);
+
+ this._applyLockscreenLayout();
+ this._startIdleWatch();
+
+ super.show();
+ }
+
+ hide() {
+ if (this._authClient && this._authClient.initialized) {
+ this._authClient.cancel();
+ }
+
+ this._stopIdleWatch();
+
+ super.hide();
+ }
+
+ addCharacter(unichar) {
+ // Add a character to the password entry (for forwarding the first keypress)
+ this._passwordEntry.clutter_text.insert_unichar(unichar);
+ }
+
+ _startIdleWatch() {
+ this._stopIdleWatch();
+ let timeout = (DEBUG_IDLE ? 5 : IDLE_TIMEOUT) * 1000;
+ this._idleWatchId = this._idleMonitor.add_idle_watch(timeout, () => {
+ this._screenShield.hideUnlockDialog();
+ });
+ }
+
+ _stopIdleWatch() {
+ if (this._idleWatchId) {
+ this._idleMonitor.remove_watch(this._idleWatchId);
+ this._idleWatchId = 0;
+ }
+ }
+
+ vfunc_destroy() {
+ if (this._sourceChangedId && this._inputSourceManager) {
+ this._inputSourceManager.disconnect(this._sourceChangedId);
+ this._sourceChangedId = 0;
+ }
+
+ if (this._authClient) {
+ if (this._authClient.initialized) {
+ this._authClient.cancel();
+ }
+ this._authClient = null;
+ }
+
+ this._stopIdleWatch();
+
+ super.vfunc_destroy();
+ }
+});
diff --git a/js/ui/screenshot.js b/js/ui/screenshot.js
index 9f6a802e24..3d2a4365d7 100644
--- a/js/ui/screenshot.js
+++ b/js/ui/screenshot.js
@@ -255,7 +255,7 @@ class SelectArea {
}
show() {
- if (!Main.pushModal(this._group))
+ if (!Main.pushModal(this._group, undefined, undefined, Cinnamon.ActionMode.SYSTEM_MODAL))
return;
this._group.connect('key-press-event', Lang.bind(this, this._onKeyPressEvent));
@@ -465,7 +465,7 @@ class PickColor {
}
show() {
- if (!Main.pushModal(this._group))
+ if (!Main.pushModal(this._group, undefined, undefined, Cinnamon.ActionMode.SYSTEM_MODAL))
return;
this._group.connect('key-press-event', Lang.bind(this, this._onKeyPressEvent));
diff --git a/js/ui/searchProviderManager.js b/js/ui/searchProviderManager.js
index 18651dfc59..acb1a14c05 100644
--- a/js/ui/searchProviderManager.js
+++ b/js/ui/searchProviderManager.js
@@ -1,7 +1,6 @@
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
const Extension = imports.ui.extension;
-const {getModuleByIndex} = imports.misc.fileUtils;
const GLib = imports.gi.GLib;
// Maps uuid -> importer object (extension directory tree)
@@ -23,7 +22,7 @@ function prepareExtensionUnload(extension) {
// Callback for extension.js
function finishExtensionLoad(extensionIndex) {
let extension = Extension.extensions[extensionIndex];
- searchProviderObj[extension.uuid] = getModuleByIndex(extension.moduleIndex);
+ searchProviderObj[extension.uuid] = extension.module;
return true;
}
diff --git a/js/ui/separator.js b/js/ui/separator.js
index 6e8296455e..1670c5efe0 100644
--- a/js/ui/separator.js
+++ b/js/ui/separator.js
@@ -1,23 +1,19 @@
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
const Cairo = imports.cairo;
-const Lang = imports.lang;
+const GObject = imports.gi.GObject;
const St = imports.gi.St;
-function Separator() {
- this._init();
-}
-
-Separator.prototype = {
- _init: function() {
- this.actor = new St.DrawingArea({ style_class: 'separator' });
- this.actor.connect('repaint', Lang.bind(this, this._onRepaint));
- },
+var Separator = GObject.registerClass(
+class Separator extends St.DrawingArea {
+ _init() {
+ super._init({ style_class: 'separator' });
+ }
- _onRepaint: function(area) {
- let cr = area.get_context();
- let themeNode = area.get_theme_node();
- let [width, height] = area.get_surface_size();
+ vfunc_repaint() {
+ let cr = this.get_context();
+ let themeNode = this.get_theme_node();
+ let [width, height] = this.get_surface_size();
let margin = themeNode.get_length('-margin-horizontal');
let gradientHeight = themeNode.get_length('-gradient-height');
let startColor = themeNode.get_color('-gradient-start');
@@ -35,4 +31,4 @@ Separator.prototype = {
cr.$dispose();
}
-};
+});
diff --git a/js/ui/userWidget.js b/js/ui/userWidget.js
index effef672c0..cdec04aa72 100644
--- a/js/ui/userWidget.js
+++ b/js/ui/userWidget.js
@@ -14,28 +14,31 @@ const Params = imports.misc.params;
var AVATAR_ICON_SIZE = 64;
var Avatar = GObject.registerClass(
-class Avatar extends Clutter.Actor {
+class Avatar extends St.Bin {
_init(user, params) {
let themeContext = St.ThemeContext.get_for_stage(global.stage);
params = Params.parse(params, {
styleClass: 'user-icon',
reactive: true,
+ track_hover: true,
iconSize: AVATAR_ICON_SIZE,
});
super._init({
- layout_manager: new Clutter.BinLayout(),
+ style_class: params.styleClass,
reactive: params.reactive,
width: params.iconSize * themeContext.scaleFactor,
height: params.iconSize * themeContext.scaleFactor,
});
- this._styleClass = params.styleClass;
+ this.connect('notify::hover', this._onHoverChanged.bind(this));
+
+ this.set_important(true);
this._iconSize = params.iconSize;
this._user = user;
- this._iconFile = null;
- this._child = null;
+ this.bind_property('reactive', this, 'track-hover',
+ GObject.BindingFlags.SYNC_CREATE);
this.bind_property('reactive', this, 'can-focus',
GObject.BindingFlags.SYNC_CREATE);
@@ -43,40 +46,36 @@ class Avatar extends Clutter.Actor {
this._scaleFactorChangeId =
themeContext.connect('notify::scale-factor', this.update.bind(this));
- // Monitor for changes to the icon file on disk
- this._textureCache = St.TextureCache.get_default();
- this._textureFileChangedId =
- this._textureCache.connect('texture-file-changed', this._onTextureFileChanged.bind(this));
-
this.connect('destroy', this._onDestroy.bind(this));
}
_onHoverChanged() {
- if (!this._child)
- return;
-
- if (this._child.hover) {
- if (this._iconFile) {
+ if (this.hover) {
+ if (this.child) {
+ this.child.add_style_class_name('highlighted');
+ }
+ else {
let effect = new Clutter.BrightnessContrastEffect();
effect.set_brightness(0.2);
effect.set_contrast(0.3);
- this._child.add_effect(effect);
- } else {
- this._child.add_style_class_name('highlighted');
+ this.add_effect(effect);
}
- this._child.add_accessible_state(Atk.StateType.FOCUSED);
+ this.add_accessible_state(Atk.StateType.FOCUSED);
} else {
- if (this._iconFile) {
- this._child.clear_effects();
- } else {
- this._child.remove_style_class_name('highlighted');
+ if (this.child) {
+ this.child.remove_style_class_name('highlighted');
}
- this._child.remove_accessible_state(Atk.StateType.FOCUSED);
+ else {
+ this.clear_effects();
+ }
+ this.remove_accessible_state(Atk.StateType.FOCUSED);
}
}
- _onStyleChanged() {
- let node = this._child.get_theme_node();
+ vfunc_style_changed() {
+ super.vfunc_style_changed();
+
+ let node = this.get_theme_node();
let [found, iconSize] = node.lookup_length('icon-size', false);
if (!found)
@@ -84,31 +83,17 @@ class Avatar extends Clutter.Actor {
let themeContext = St.ThemeContext.get_for_stage(global.stage);
- // node.lookup_length() returns a scaled value, but we need unscaled
- let newIconSize = iconSize / themeContext.scaleFactor;
-
- if (newIconSize !== this._iconSize) {
- this._iconSize = newIconSize;
- this.update();
- }
+ // node.lookup_length() returns a scaled value, but we
+ // need unscaled
+ this._iconSize = iconSize / themeContext.scaleFactor;
+ this.update();
}
_onDestroy() {
if (this._scaleFactorChangeId) {
let themeContext = St.ThemeContext.get_for_stage(global.stage);
themeContext.disconnect(this._scaleFactorChangeId);
- this._scaleFactorChangeId = 0;
- }
-
- if (this._textureFileChangedId) {
- this._textureCache.disconnect(this._textureFileChangedId);
- this._textureFileChangedId = 0;
- }
- }
-
- _onTextureFileChanged(cache, file) {
- if (this._iconFile && file.get_path() === this._iconFile) {
- this.update();
+ delete this._scaleFactorChangeId;
}
}
@@ -129,49 +114,24 @@ class Avatar extends Clutter.Actor {
iconFile = null;
}
- this._iconFile = iconFile;
-
let { scaleFactor } = St.ThemeContext.get_for_stage(global.stage);
this.set_size(
this._iconSize * scaleFactor,
this._iconSize * scaleFactor);
- // Remove old child
- if (this._child) {
- this._child.destroy();
- this._child = null;
- }
-
- let size = this._iconSize * scaleFactor;
-
if (iconFile) {
- this._child = new St.Bin({
- style_class: `${this._styleClass} user-avatar`,
- reactive: this.reactive,
- track_hover: this.reactive,
- width: size,
- height: size,
- style: `background-image: url("${iconFile}"); background-size: cover;`,
- });
+ this.child = null;
+ this.add_style_class_name('user-avatar');
+ this.style = `
+ background-image: url("${iconFile}");
+ background-size: cover;`;
} else {
- this._child = new St.Bin({
- style_class: this._styleClass,
- reactive: this.reactive,
- track_hover: this.reactive,
- width: size,
- height: size,
- child: new St.Icon({
- icon_name: 'xsi-avatar-default-symbolic',
- icon_size: this._iconSize,
- }),
+ this.style = null;
+ this.child = new St.Icon({
+ icon_name: 'xsi-avatar-default-symbolic',
+ icon_size: this._iconSize,
});
}
-
- this._child.set_important(true);
- this._child.connect('notify::hover', this._onHoverChanged.bind(this));
- this._child.connect('style-changed', this._onStyleChanged.bind(this));
-
- this.add_child(this._child);
}
});
@@ -195,7 +155,7 @@ class UserWidget extends St.BoxLayout {
this._avatar = new Avatar(user);
this._avatar.x_align = Clutter.ActorAlign.CENTER;
- this.add_child(this._avatar);
+ this.add(this._avatar, { x_fill: false });
this._label = new St.Label({ style_class: 'user-widget-label' });
this._label.y_align = Clutter.ActorAlign.CENTER;
diff --git a/js/ui/virtualKeyboard.js b/js/ui/virtualKeyboard.js
index 17748c6204..892f0924b9 100644
--- a/js/ui/virtualKeyboard.js
+++ b/js/ui/virtualKeyboard.js
@@ -12,6 +12,8 @@ const Main = imports.ui.main;
const PageIndicators = imports.ui.pageIndicators;
const PopupMenu = imports.ui.popupMenu;
+let _popupContainer = null;
+
var KEYBOARD_REST_TIME = 50;
var KEY_LONG_PRESS_TIME = 250;
var PANEL_SWITCH_ANIMATION_TIME = 500;
@@ -280,7 +282,11 @@ var Key = GObject.registerClass({
this._boxPointer = new BoxPointer.BoxPointer(St.Side.BOTTOM);
this._boxPointer.hide();
- Main.layoutManager.addChrome(this._boxPointer);
+ if (_popupContainer) {
+ _popupContainer.add_child(this._boxPointer);
+ } else {
+ Main.layoutManager.addChrome(this._boxPointer);
+ }
this._boxPointer.setPosition(this.keyButton, 0.5);
// Adds style to existing keyboard style to avoid repetition
@@ -687,6 +693,7 @@ var VirtualKeyboardManager = GObject.registerClass({
constructor() {
super();
this._keyboard = null;
+ this._screensaverMode = false;
this._a11yApplicationsSettings = new Gio.Settings({ schema_id: A11Y_APPLICATIONS_SCHEMA });
this._a11yApplicationsSettings.connect('changed::screen-keyboard-enabled', this._keyboardEnabledChanged.bind(this));
@@ -732,6 +739,9 @@ var VirtualKeyboardManager = GObject.registerClass({
}
_syncEnabled() {
+ if (this._screensaverMode)
+ return;
+
let enabled = this._shouldEnable();
if (!enabled && !this._keyboard)
return;
@@ -744,6 +754,9 @@ var VirtualKeyboardManager = GObject.registerClass({
}
destroyKeyboard() {
+ if (this._screensaverMode)
+ return;
+
if (this._keyboard == null) {
return;
}
@@ -810,6 +823,35 @@ var VirtualKeyboardManager = GObject.registerClass({
return Main.layoutManager.keyboardBox.contains(actor) ||
!!actor._extendedKeys || !!actor.extendedKey;
}
+
+ ensureKeyboard() {
+ if (!this._keyboard)
+ this._keyboard = new Keyboard(true);
+ return this._keyboard;
+ }
+
+ openForScreensaver(keyboardContainer, popupContainer) {
+ this._screensaverMode = true;
+ let keyboard = this.ensureKeyboard();
+ Main.layoutManager.untrackChrome(keyboard);
+ global.reparentActor(keyboard, keyboardContainer);
+ keyboard.setScreensaverMode(true, popupContainer);
+ }
+
+ closeForScreensaver() {
+ if (!this._keyboard) {
+ this._screensaverMode = false;
+ return;
+ }
+
+ this._keyboard.setScreensaverMode(false);
+ global.reparentActor(this._keyboard, Main.layoutManager.keyboardBox);
+ Main.layoutManager.trackChrome(this._keyboard);
+ this._screensaverMode = false;
+
+ if (!this._shouldEnable())
+ this.destroyKeyboard();
+ }
});
var Keyboard = GObject.registerClass(
@@ -856,6 +898,7 @@ class Keyboard extends St.BoxLayout {
this._keyboardVisible = visible;
});
this._keyboardRequested = false;
+ this._screensaverMode = false;
this._keyboardRestingId = 0;
this._connectSignal(Main.layoutManager, 'monitors-changed', this._relayout.bind(this));
@@ -892,8 +935,10 @@ class Keyboard extends St.BoxLayout {
this._keyboardController.destroy();
- Main.layoutManager.untrackChrome(this);
- Main.layoutManager.keyboardBox.remove_actor(this);
+ if (!this._screensaverMode) {
+ Main.layoutManager.untrackChrome(this);
+ Main.layoutManager.keyboardBox.remove_actor(this);
+ }
if (this._languagePopup) {
this._languagePopup.destroy();
@@ -954,6 +999,9 @@ class Keyboard extends St.BoxLayout {
}
_onKeyFocusChanged() {
+ if (this._screensaverMode)
+ return;
+
let focus = global.stage.key_focus;
// Showing an extended key popup and clicking a key from the extended keys
@@ -1252,6 +1300,9 @@ class Keyboard extends St.BoxLayout {
}
_relayout() {
+ if (this._screensaverMode)
+ return;
+
this._suggestions.visible = this._keyboardController.getIbusInputActive();
let monitor = Main.layoutManager.keyboardMonitor;
@@ -1313,6 +1364,9 @@ class Keyboard extends St.BoxLayout {
}
_onKeyboardStateChanged(controller, state) {
+ if (this._screensaverMode)
+ return;
+
let enabled;
if (state == Clutter.InputPanelState.OFF)
enabled = false;
@@ -1365,6 +1419,9 @@ class Keyboard extends St.BoxLayout {
}
open(monitor) {
+ if (this._screensaverMode)
+ return;
+
this._clearShowIdle();
this._keyboardRequested = true;
@@ -1397,6 +1454,11 @@ class Keyboard extends St.BoxLayout {
}
close() {
+ if (this._screensaverMode) {
+ Main.screenShieldHideKeyboard();
+ return;
+ }
+
this._clearShowIdle();
this._keyboardRequested = false;
@@ -1440,6 +1502,48 @@ class Keyboard extends St.BoxLayout {
GLib.source_remove(this._showIdleId);
this._showIdleId = 0;
}
+
+ setScreensaverMode(active, popupContainer = null) {
+ // Clear extended key popups before changing container so they
+ // are destroyed from their current parent context.
+ this._clearExtendedKeyPopups();
+
+ this._screensaverMode = active;
+ _popupContainer = active ? popupContainer : null;
+
+ if (active) {
+ this._keyboardVisible = true;
+ this._keyboardRequested = true;
+ } else {
+ this._keyboardVisible = false;
+ this._keyboardRequested = false;
+ this._relayout();
+ }
+ }
+
+ _clearExtendedKeyPopups() {
+ if (!this._groups)
+ return;
+
+ for (let groupName in this._groups) {
+ let layers = this._groups[groupName];
+ if (!layers)
+ continue;
+
+ for (let level in layers) {
+ let keyContainer = layers[level];
+ if (!keyContainer)
+ continue;
+
+ for (let child of keyContainer.get_children()) {
+ if (child._boxPointer) {
+ child._boxPointer.destroy();
+ child._boxPointer = null;
+ }
+ }
+ }
+ }
+ }
});
var KeyboardController = class {
diff --git a/js/ui/windowManager.js b/js/ui/windowManager.js
index d95dce4a13..dfb0cbadb9 100644
--- a/js/ui/windowManager.js
+++ b/js/ui/windowManager.js
@@ -841,11 +841,16 @@ var WindowManager = class WindowManager {
}
_filterKeybinding(shellwm, binding) {
- // TODO: We can use ActionModes to manage what keybindings are
- // available where. For now, this allows global keybindings in a non-
- // modal state.
+ // Builtin keybindings (defined by Muffin) work in NORMAL mode by default
+ if (Main.actionMode == Cinnamon.ActionMode.NORMAL && binding.is_builtin())
+ return false;
+
+ // Look up the binding in our keybinding manager
+ let bindingName = binding.get_name();
+ let [action_id, entry] = Main.keybindingManager._lookupEntry(bindingName);
- return global.stage_input_mode !== Cinnamon.StageInputMode.NORMAL;
+ // Use the common filtering logic from main.js
+ return Main._shouldFilterKeybinding(entry);
}
_hasAttachedDialogs(window, ignoreWindow) {
diff --git a/js/ui/windowMenu.js b/js/ui/windowMenu.js
index 1cf0d3fa7e..e0cf2fc672 100644
--- a/js/ui/windowMenu.js
+++ b/js/ui/windowMenu.js
@@ -81,30 +81,32 @@ var MnemonicLeftOrnamentedMenuItem = class MnemonicLeftOrnamentedMenuItem extend
setOrnament(ornamentType, state) {
switch (ornamentType) {
case PopupMenu.OrnamentType.CHECK:
- if ((this._ornament.child)&&(!(this._ornament.child._delegate instanceof CheckBox.CheckButton))) {
+ if ((this._ornament.child) && (!(this._ornament.child._delegate instanceof CheckBox.CheckBox))) {
this._ornament.child.destroy();
this._ornament.child = null;
}
if (!this._ornament.child) {
- let switchOrn = new CheckBox.CheckButton(state);
- this._ornament.child = switchOrn.actor;
- switchOrn.actor.reactive = false;
+ let switchOrn = new CheckBox.CheckBox();
+ switchOrn.set_checked(state);
+ this._ornament.child = switchOrn;
+ switchOrn.reactive = false;
} else {
- this._ornament.child._delegate.setToggleState(state);
+ this._ornament.child.set_checked(state);
}
this._icon = null;
break;
case PopupMenu.OrnamentType.DOT:
- if ((this._ornament.child)&&(!(this._ornament.child._delegate instanceof RadioButton.RadioBox))) {
+ if ((this._ornament.child) && (!(this._ornament.child._delegate instanceof RadioButton.RadioButton))) {
this._ornament.child.destroy();
this._ornament.child = null;
}
if (!this._ornament.child) {
- let radioOrn = new RadioButton.RadioBox(state);
- this._ornament.child = radioOrn.actor;
- radioOrn.actor.reactive = false;
+ let radioOrn = new RadioButton.RadioButton();
+ radioOrn.set_checked(state);
+ this._ornament.child = radioOrn;
+ radioOrn.reactive = false;
} else {
- this._ornament.child._delegate.setToggleState(state);
+ this._ornament.child.set_checked(state);
}
this._icon = null;
break;
diff --git a/meson.build b/meson.build
index d851f539f4..d27f497085 100644
--- a/meson.build
+++ b/meson.build
@@ -12,6 +12,7 @@ datadir = get_option('datadir')
libdir = join_paths(prefix, get_option('libdir'))
includedir = get_option('includedir')
libexecdir = get_option('libexecdir')
+sysconfdir = join_paths(prefix, get_option('sysconfdir'))
desktopdir = join_paths(datadir, 'applications')
x_sessiondir = join_paths(datadir, 'xsessions')
wayland_sessiondir = join_paths(datadir, 'wayland-sessions')
@@ -45,6 +46,8 @@ muffin_typelibdir = muffin.get_variable(pkgconfig: 'typelibdir')
pango = dependency('muffin-cogl-pango-0')
xapp = dependency('xapp', version: '>= 2.6.0')
X11 = dependency('x11')
+xcomposite = dependency('xcomposite')
+xext = dependency('xext')
xml = dependency('libxml-2.0')
nm_deps = []
@@ -79,6 +82,27 @@ message('Building recorder: @0@'.format(get_option('build_recorder')))
cc = meson.get_compiler('c')
math = cc.find_library('m', required: false)
+xdo = dependency('libxdo', required: false)
+if not xdo.found()
+ xdo = cc.find_library('xdo')
+endif
+
+# PAM authentication library
+pam_compile = '''#include
+ #include
+ #include
+ int main ()
+ {
+ pam_handle_t *pamh = 0;
+ char *s = pam_strerror(pamh, PAM_SUCCESS);
+ return 0;
+ }'''
+
+pam = cc.find_library('pam')
+if not cc.has_function('sigtimedwait')
+ pam = [pam, cc.find_library('rt')]
+endif
+
python = find_program('python3')
# generate config.h
@@ -92,6 +116,24 @@ if have_mallinfo
cinnamon_conf.set10('HAVE_MALLINFO', true)
endif
+# Check for unistd.h (needed by PAM helper)
+if cc.has_header('unistd.h')
+ cinnamon_conf.set('HAVE_UNISTD_H', true)
+endif
+
+# Check for sigaction (needed by PAM helper)
+if cc.has_function('sigaction', args: '-D_GNU_SOURCE')
+ cinnamon_conf.set('HAVE_SIGACTION', true)
+endif
+
+# PAM configuration checks
+if cc.compiles(pam_compile, dependencies: pam)
+ cinnamon_conf.set('PAM_STRERROR_TWO_ARGS', 1)
+endif
+if cc.has_function('pam_syslog', dependencies: pam)
+ cinnamon_conf.set('HAVE_PAM_SYSLOG', 1)
+endif
+
langinfo_test = '''
#include
int main () {
@@ -106,6 +148,11 @@ if have_nl_time_first_weekday
cinnamon_conf.set10('HAVE__NL_TIME_FIRST_WEEKDAY', true)
endif
+# Check for X11 Shape extension (needed by backup-locker)
+if cc.has_header('X11/extensions/shape.h', dependencies: [X11, xext])
+ cinnamon_conf.set('HAVE_SHAPE_EXT', 1)
+endif
+
config_h_file = configure_file(
output : 'config.h',
configuration : cinnamon_conf
@@ -161,6 +208,7 @@ config_js_conf = configuration_data()
config_js_conf.set('PACKAGE_NAME', meson.project_name().to_lower())
config_js_conf.set('PACKAGE_VERSION', version)
config_js_conf.set10('BUILT_NM_AGENT', internal_nm_agent)
+config_js_conf.set('LIBEXECDIR', join_paths(prefix, libexecdir))
configure_file(
input: 'js/misc/config.js.in',
diff --git a/meson_options.txt b/meson_options.txt
index cab192005c..6821b437d2 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -39,4 +39,14 @@ option('exclude_users_settings',
value : false,
description: 'Exclude Users settings'
)
+option('pam_prefix',
+ type : 'string',
+ value : '',
+ description: 'Specify where pam files go'
+)
+option('use_debian_pam',
+ type : 'boolean',
+ value : false,
+ description: 'Use the debian pam file'
+)
diff --git a/src/cinnamon-global.c b/src/cinnamon-global.c
index d50fcef3a6..b1a23bee49 100644
--- a/src/cinnamon-global.c
+++ b/src/cinnamon-global.c
@@ -1668,3 +1668,20 @@ cinnamon_global_alloc_leak (CinnamonGlobal *global, gint mb)
);
}
}
+
+/**
+ * cinnamon_global_get_stage_xwindow:
+ * @global: A #CinnamonGlobal
+ *
+ * Returns the X11 window ID of the stage window backing the compositor.
+ * This can be used to monitor cinnamon's liveness from external processes.
+ *
+ * Returns: The X11 Window ID, or 0 if not available
+ */
+gulong
+cinnamon_global_get_stage_xwindow (CinnamonGlobal *global)
+{
+ g_return_val_if_fail (CINNAMON_IS_GLOBAL (global), 0);
+
+ return meta_get_stage_xwindow (global->meta_display);
+}
diff --git a/src/cinnamon-global.h b/src/cinnamon-global.h
index ec34ffc25c..7f9c0f09a0 100644
--- a/src/cinnamon-global.h
+++ b/src/cinnamon-global.h
@@ -127,6 +127,8 @@ void cinnamon_global_segfault (CinnamonGlobal *global);
void cinnamon_global_alloc_leak (CinnamonGlobal *global,
gint mb);
+gulong cinnamon_global_get_stage_xwindow (CinnamonGlobal *global);
+
G_END_DECLS
#endif /* __CINNAMON_GLOBAL_H__ */
diff --git a/src/cinnamon-screen.c b/src/cinnamon-screen.c
index 0f12e51501..3b16bbb888 100644
--- a/src/cinnamon-screen.c
+++ b/src/cinnamon-screen.c
@@ -614,13 +614,13 @@ cinnamon_screen_get_monitor_index_for_rect (CinnamonScreen *screen,
*
* Return value: a monitor index
*
- * Deprecated: 6.4: Use meta_display_get_current_monitor() via global.display instead.
+ * Deprecated: 6.4: Use Main.layoutManager.currentMonitor.index or global.display.get_current_monitor() instead.
*/
int
cinnamon_screen_get_current_monitor (CinnamonScreen *screen)
{
g_return_val_if_fail (CINNAMON_IS_SCREEN (screen), 0);
- g_warning_once ("global.screen.get_current_monitor() is deprecated. Use global.display.get_current_monitor() instead.");
+ g_warning_once ("global.screen.get_current_monitor() is deprecated. Use Main.layoutManager.currentMonitor.index or global.display.get_current_monitor() instead.");
return meta_display_get_current_monitor (screen->display);
}
@@ -633,13 +633,13 @@ cinnamon_screen_get_current_monitor (CinnamonScreen *screen)
*
* Return value: the number of monitors
*
- * Deprecated: 6.4: Use meta_display_get_n_monitors() via global.display instead.
+ * Deprecated: 6.4: Use Main.layoutManager.monitors.length or global.display.get_n_monitors() instead.
*/
int
cinnamon_screen_get_n_monitors (CinnamonScreen *screen)
{
g_return_val_if_fail (CINNAMON_IS_SCREEN (screen), 1);
- g_warning_once ("global.screen.get_n_monitors() is deprecated. Use global.display.get_n_monitors() instead.");
+ g_warning_once ("global.screen.get_n_monitors() is deprecated. Use Main.layoutManager.monitors.length or global.display.get_n_monitors() instead.");
return meta_display_get_n_monitors (screen->display);
}
@@ -652,13 +652,13 @@ cinnamon_screen_get_n_monitors (CinnamonScreen *screen)
*
* Return value: a monitor index
*
- * Deprecated: 6.4: Use meta_display_get_primary_monitor() via global.display instead.
+ * Deprecated: 6.4: Use Main.layoutManager.primaryIndex or global.display.get_primary_monitor() instead.
*/
int
cinnamon_screen_get_primary_monitor (CinnamonScreen *screen)
{
g_return_val_if_fail (CINNAMON_IS_SCREEN (screen), 0);
- g_warning_once ("global.screen.get_primary_monitor() is deprecated. Use global.display.get_primary_monitor() instead.");
+ g_warning_once ("global.screen.get_primary_monitor() is deprecated. Use Main.layoutManager.primaryIndex or global.display.get_primary_monitor() instead.");
return meta_display_get_primary_monitor (screen->display);
}
@@ -671,7 +671,7 @@ cinnamon_screen_get_primary_monitor (CinnamonScreen *screen)
*
* Stores the location and size of the indicated monitor in @geometry.
*
- * Deprecated: 6.4: Use meta_display_get_monitor_geometry() via global.display instead.
+ * Deprecated: 6.4: Use Main.layoutManager.monitors[index] or global.display.get_monitor_geometry() instead.
*/
void
cinnamon_screen_get_monitor_geometry (CinnamonScreen *screen,
@@ -681,7 +681,7 @@ cinnamon_screen_get_monitor_geometry (CinnamonScreen *screen,
g_return_if_fail (CINNAMON_IS_SCREEN (screen));
g_return_if_fail (monitor >= 0 && monitor < meta_display_get_n_monitors (screen->display));
g_return_if_fail (geometry != NULL);
- g_warning_once ("global.screen.get_monitor_geometry() is deprecated. Use global.display.get_monitor_geometry() instead.");
+ g_warning_once ("global.screen.get_monitor_geometry() is deprecated. Use Main.layoutManager.monitors[index] (has x, y, width, height) or global.display.get_monitor_geometry() instead.");
meta_display_get_monitor_geometry (screen->display, monitor, geometry);
}
@@ -871,7 +871,7 @@ cinnamon_screen_get_active_workspace (CinnamonScreen *screen)
*
* Returns: %TRUE if there is a fullscreen window covering the specified monitor.
*
- * Deprecated: 6.4: Use meta_display_get_monitor_in_fullscreen() via global.display instead.
+ * Deprecated: 6.4: Use Main.layoutManager.monitors[index].inFullscreen or global.display.get_monitor_in_fullscreen() instead.
*/
gboolean
cinnamon_screen_get_monitor_in_fullscreen (CinnamonScreen *screen,
@@ -880,7 +880,7 @@ cinnamon_screen_get_monitor_in_fullscreen (CinnamonScreen *screen,
g_return_val_if_fail (CINNAMON_IS_SCREEN (screen), FALSE);
g_return_val_if_fail (monitor >= 0 &&
monitor < meta_display_get_n_monitors (screen->display), FALSE);
- g_warning_once ("global.screen.get_monitor_in_fullscreen() is deprecated. Use global.display.get_monitor_in_fullscreen() instead.");
+ g_warning_once ("global.screen.get_monitor_in_fullscreen() is deprecated. Use Main.layoutManager.monitors[index].inFullscreen or global.display.get_monitor_in_fullscreen() instead.");
return meta_display_get_monitor_in_fullscreen (screen->display, monitor);
}
diff --git a/src/meson.build b/src/meson.build
index a44cb7c0e9..925b67226e 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -198,6 +198,8 @@ executable(
install: true,
)
+subdir('screensaver')
+
cinnamon_gir_includes = [
'Clutter-0',
'ClutterX11-0',
diff --git a/src/screensaver/backup-locker/backup-locker.c b/src/screensaver/backup-locker/backup-locker.c
new file mode 100644
index 0000000000..d1e6781990
--- /dev/null
+++ b/src/screensaver/backup-locker/backup-locker.c
@@ -0,0 +1,1178 @@
+#include "config.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "gdk-event-filter.h"
+#include "event-grabber.h"
+
+#define BUS_NAME "org.cinnamon.BackupLocker"
+#define BUS_PATH "/org/cinnamon/BackupLocker"
+
+#define LAUNCHER_BUS_NAME "org.cinnamon.Launcher"
+#define LAUNCHER_BUS_PATH "/org/cinnamon/Launcher"
+#define LAUNCHER_INTERFACE "org.cinnamon.Launcher"
+
+#define BACKUP_TYPE_LOCKER (backup_locker_get_type ())
+G_DECLARE_FINAL_TYPE (BackupLocker, backup_locker, BACKUP, LOCKER, GtkApplication)
+
+struct _BackupLocker
+{
+ GtkApplication parent_instance;
+
+ GtkWidget *window;
+ GtkWidget *fixed;
+ GtkWidget *info_box;
+ GtkWidget *stack;
+ GtkWidget *recover_button;
+
+ CsGdkEventFilter *event_filter;
+ CsEventGrabber *grabber;
+
+ GCancellable *monitor_cancellable;
+ GMutex pretty_xid_mutex;
+
+ gulong pretty_xid;
+ guint activate_idle_id;
+ guint sigterm_src_id;
+ guint recover_timeout_id;
+ guint can_restart_check_id;
+ gint can_restart_retries;
+ guint term_tty;
+ guint session_tty;
+
+ Display *cow_display;
+
+ gboolean should_grab;
+ gboolean hold_mode;
+ gboolean locked;
+};
+
+G_DEFINE_TYPE (BackupLocker, backup_locker, GTK_TYPE_APPLICATION)
+
+static GDBusNodeInfo *introspection_data = NULL;
+
+static const gchar introspection_xml[] =
+ ""
+ " "
+ " "
+ " "
+ " "
+ " "
+ " "
+ " "
+ " "
+ " "
+ " "
+ "";
+
+static void create_window (BackupLocker *self);
+static void setup_window_monitor (BackupLocker *self, gulong xid);
+static void release_grabs_internal (BackupLocker *self);
+static void check_can_restart (BackupLocker *self);
+
+static gboolean cow_x_error;
+
+static int
+cow_x_error_handler (Display *display, XErrorEvent *event)
+{
+ cow_x_error = TRUE;
+ return 0;
+}
+
+static void
+acquire_cow (BackupLocker *self)
+{
+ int (*old_handler) (Display *, XErrorEvent *);
+
+ if (self->cow_display != NULL)
+ return;
+
+ self->cow_display = XOpenDisplay (NULL);
+
+ if (self->cow_display == NULL)
+ {
+ g_warning ("Failed to open X display for COW");
+ return;
+ }
+
+ cow_x_error = FALSE;
+ old_handler = XSetErrorHandler (cow_x_error_handler);
+
+ XCompositeGetOverlayWindow (self->cow_display,
+ DefaultRootWindow (self->cow_display));
+ XSync (self->cow_display, False);
+
+ XSetErrorHandler (old_handler);
+
+ if (cow_x_error)
+ {
+ g_warning ("acquire_cow: X error getting composite overlay window");
+ XCloseDisplay (self->cow_display);
+ self->cow_display = NULL;
+ return;
+ }
+
+ g_debug ("acquire_cow: holding composite overlay window");
+}
+
+static void
+release_cow (BackupLocker *self)
+{
+ int (*old_handler) (Display *, XErrorEvent *);
+
+ if (self->cow_display == NULL)
+ return;
+
+ g_debug ("release_cow: releasing composite overlay window");
+
+ cow_x_error = FALSE;
+ old_handler = XSetErrorHandler (cow_x_error_handler);
+
+ XCompositeReleaseOverlayWindow (self->cow_display,
+ DefaultRootWindow (self->cow_display));
+ XSync (self->cow_display, False);
+
+ XSetErrorHandler (old_handler);
+
+ if (cow_x_error)
+ g_warning ("release_cow: X error releasing composite overlay window");
+
+ XCloseDisplay (self->cow_display);
+ self->cow_display = NULL;
+}
+
+static void
+set_net_wm_name (GdkWindow *window,
+ const gchar *name)
+{
+ GdkDisplay *display = gdk_display_get_default ();
+ Window xwindow = gdk_x11_window_get_xid (window);
+
+ gdk_x11_display_error_trap_push (display);
+
+ XChangeProperty (GDK_DISPLAY_XDISPLAY (display), xwindow,
+ gdk_x11_get_xatom_by_name_for_display (display, "_NET_WM_NAME"),
+ gdk_x11_get_xatom_by_name_for_display (display, "UTF8_STRING"), 8,
+ PropModeReplace, (guchar *)name, strlen (name));
+
+ XFlush (GDK_DISPLAY_XDISPLAY (display));
+
+ gdk_x11_display_error_trap_pop_ignored (display);
+}
+
+static void
+position_info_box (BackupLocker *self)
+{
+ GdkDisplay *display;
+ GdkMonitor *monitor;
+ GdkRectangle rect;
+ GtkRequisition natural_size;
+
+ if (self->info_box == NULL)
+ return;
+
+ gtk_widget_get_preferred_size (self->info_box, NULL, &natural_size);
+
+ if (natural_size.width == 0 || natural_size.height == 0)
+ return;
+
+ display = gdk_display_get_default ();
+ monitor = gdk_display_get_primary_monitor (display);
+ gdk_monitor_get_workarea (monitor, &rect);
+
+ g_debug ("Positioning info box (%dx%d) to primary monitor (%d+%d+%dx%d)",
+ natural_size.width, natural_size.height,
+ rect.x, rect.y, rect.width, rect.height);
+
+ gtk_fixed_move (GTK_FIXED (self->fixed), self->info_box,
+ rect.x + (rect.width / 2) - (natural_size.width / 2),
+ rect.y + (rect.height / 2) - (natural_size.height / 2));
+}
+
+static void
+root_window_size_changed (CsGdkEventFilter *filter,
+ gpointer user_data)
+{
+ BackupLocker *self = BACKUP_LOCKER (user_data);
+ GdkWindow *gdk_win;
+ Display *xdisplay;
+ gint w, h, screen_num;
+
+ gdk_win = gtk_widget_get_window (self->window);
+
+ xdisplay = GDK_DISPLAY_XDISPLAY (gdk_window_get_display (gdk_win));
+ screen_num = DefaultScreen (xdisplay);
+
+ w = DisplayWidth (xdisplay, screen_num);
+ h = DisplayHeight (xdisplay, screen_num);
+
+ gdk_window_move_resize (gtk_widget_get_window (self->window),
+ 0, 0, w, h);
+ position_info_box (self);
+
+ gtk_widget_queue_resize (self->window);
+}
+
+static void window_grab_broken (gpointer data);
+
+static gboolean
+activate_backup_window_cb (BackupLocker *self)
+{
+ g_debug ("activate_backup_window_cb: should_grab=%d", self->should_grab);
+
+ if (self->should_grab)
+ {
+ if (cs_event_grabber_grab_root (self->grabber, FALSE))
+ {
+ guint32 user_time;
+ cs_event_grabber_move_to_window (self->grabber,
+ gtk_widget_get_window (self->window),
+ gtk_widget_get_screen (self->window),
+ FALSE);
+ g_signal_connect_swapped (self->window, "grab-broken-event", G_CALLBACK (window_grab_broken), self);
+
+ user_time = gdk_x11_display_get_user_time (gtk_widget_get_display (self->window));
+ gdk_x11_window_set_user_time (gtk_widget_get_window (self->window), user_time);
+
+ gtk_widget_set_sensitive (self->recover_button, FALSE);
+ gtk_stack_set_visible_child_name (GTK_STACK (self->stack), "auto");
+ gtk_widget_show (self->info_box);
+ position_info_box (self);
+
+ self->can_restart_retries = 0;
+ g_clear_handle_id (&self->can_restart_check_id, g_source_remove);
+ check_can_restart (self);
+ }
+ else
+ {
+ return G_SOURCE_CONTINUE;
+ }
+ }
+
+ self->activate_idle_id = 0;
+ return G_SOURCE_REMOVE;
+}
+
+static void
+activate_backup_window (BackupLocker *self)
+{
+ g_clear_handle_id (&self->activate_idle_id, g_source_remove);
+ self->activate_idle_id = g_timeout_add (20, (GSourceFunc) activate_backup_window_cb, self);
+}
+
+static void
+ungrab (BackupLocker *self)
+{
+ cs_event_grabber_release (self->grabber);
+ self->should_grab = FALSE;
+}
+
+static void
+window_grab_broken (gpointer data)
+{
+ BackupLocker *self = BACKUP_LOCKER (data);
+
+ g_signal_handlers_disconnect_by_func (self->window, window_grab_broken, self);
+
+ if (self->should_grab)
+ {
+ g_debug ("Grab broken, retrying");
+ activate_backup_window (self);
+ }
+}
+
+static gboolean
+update_for_compositing (BackupLocker *self)
+{
+ GdkVisual *visual;
+
+ if (self->should_grab)
+ {
+ cs_event_grabber_release (self->grabber);
+ }
+
+ if (gdk_screen_is_composited (gdk_screen_get_default ()))
+ {
+ visual = gdk_screen_get_rgba_visual (gdk_screen_get_default ());
+ if (!visual)
+ {
+ g_critical ("Can't get RGBA visual to paint backup window");
+ return FALSE;
+ }
+ }
+ else
+ {
+ visual = gdk_screen_get_system_visual (gdk_screen_get_default ());
+ }
+
+ g_debug ("update for compositing (composited: %s)",
+ gdk_screen_is_composited (gdk_screen_get_default ()) ? "yes" : "no");
+
+ gtk_widget_hide (self->window);
+ gtk_widget_unrealize (self->window);
+ gtk_widget_set_visual (self->window, visual);
+ gtk_widget_realize (self->window);
+
+ if (self->locked)
+ gtk_widget_show (self->window);
+
+ if (self->should_grab)
+ activate_backup_window (self);
+
+ return TRUE;
+}
+
+static void
+on_composited_changed (BackupLocker *self)
+{
+ g_debug ("Received composited-changed (composited: %s)",
+ gdk_screen_is_composited (gdk_screen_get_default ()) ? "yes" : "no");
+
+ if (self->window == NULL || !self->locked)
+ return;
+
+ if (!update_for_compositing (self))
+ {
+ g_critical ("Error realizing backup-locker window - exiting");
+
+ if (self->locked)
+ {
+ self->locked = FALSE;
+ g_application_release (G_APPLICATION (self));
+ }
+ }
+}
+
+static void
+on_window_realize (GtkWidget *widget, BackupLocker *self)
+{
+ GdkWindow *gdk_win = gtk_widget_get_window (widget);
+
+ g_debug ("on_window_realize: window xid=0x%lx",
+ (gulong) GDK_WINDOW_XID (gdk_win));
+
+ set_net_wm_name (gdk_win, "backup-locker");
+
+ root_window_size_changed (self->event_filter, self);
+}
+
+static void
+show_manual_instructions (BackupLocker *self)
+{
+ g_clear_handle_id (&self->recover_timeout_id, g_source_remove);
+ gtk_stack_set_visible_child_name (GTK_STACK (self->stack), "manual");
+}
+
+static gboolean
+recover_timeout_cb (gpointer data)
+{
+ BackupLocker *self = BACKUP_LOCKER (data);
+
+ g_debug ("Recovery timeout, showing manual instructions");
+
+ self->recover_timeout_id = 0;
+ show_manual_instructions (self);
+
+ return G_SOURCE_REMOVE;
+}
+
+static gboolean
+retry_can_restart_cb (gpointer data)
+{
+ BackupLocker *self = BACKUP_LOCKER (data);
+
+ self->can_restart_check_id = 0;
+ check_can_restart (self);
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+on_can_restart_reply (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ BackupLocker *self = BACKUP_LOCKER (user_data);
+ GVariant *ret;
+ GError *error = NULL;
+ gboolean can_restart = FALSE;
+
+ ret = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), result, &error);
+
+ if (error != NULL)
+ {
+ g_debug ("CanRestart call failed: %s", error->message);
+ g_error_free (error);
+ }
+ else
+ {
+ g_variant_get (ret, "(b)", &can_restart);
+ g_variant_unref (ret);
+ }
+
+ if (can_restart)
+ {
+ gtk_widget_set_sensitive (self->recover_button, TRUE);
+ }
+ else if (self->can_restart_retries < 5)
+ {
+ self->can_restart_retries++;
+ self->can_restart_check_id = g_timeout_add_seconds (1, retry_can_restart_cb, self);
+ }
+ else
+ {
+ show_manual_instructions (self);
+ }
+}
+
+static void
+check_can_restart (BackupLocker *self)
+{
+ self->can_restart_check_id = 0;
+
+ g_dbus_connection_call (g_application_get_dbus_connection (G_APPLICATION (self)),
+ LAUNCHER_BUS_NAME,
+ LAUNCHER_BUS_PATH,
+ LAUNCHER_INTERFACE,
+ "CanRestart",
+ NULL,
+ G_VARIANT_TYPE ("(b)"),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ NULL,
+ on_can_restart_reply,
+ self);
+}
+static void
+on_recover_finished (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ BackupLocker *self = BACKUP_LOCKER (user_data);
+ GVariant *ret;
+ GError *error = NULL;
+ gboolean success = FALSE;
+
+ ret = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), result, &error);
+
+ if (error != NULL)
+ {
+ g_warning ("TryRestart failed: %s", error->message);
+ g_error_free (error);
+ show_manual_instructions (self);
+ return;
+ }
+
+ g_variant_get (ret, "(b)", &success);
+ g_variant_unref (ret);
+
+ if (!success)
+ {
+ show_manual_instructions (self);
+ }
+}
+
+static void
+on_recover_clicked (GtkButton *button, BackupLocker *self)
+{
+ g_debug ("Attempting automatic recovery");
+
+ gtk_widget_set_sensitive (GTK_WIDGET (button), FALSE);
+
+ g_dbus_connection_call (g_application_get_dbus_connection (G_APPLICATION (self)),
+ LAUNCHER_BUS_NAME,
+ LAUNCHER_BUS_PATH,
+ LAUNCHER_INTERFACE,
+ "TryRestart",
+ NULL,
+ G_VARIANT_TYPE ("(b)"),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ NULL,
+ on_recover_finished,
+ self);
+
+ self->recover_timeout_id = g_timeout_add_seconds (10, recover_timeout_cb, self);
+}
+
+static void
+create_window (BackupLocker *self)
+{
+ GtkWidget *box;
+ GtkWidget *widget;
+ GtkStyleContext *context;
+ GtkCssProvider *provider;
+ PangoAttrList *attrs;
+ GdkVisual *visual;
+
+ self->window = g_object_new (GTK_TYPE_WINDOW,
+ "type", GTK_WINDOW_POPUP,
+ NULL);
+
+ gtk_widget_set_events (self->window,
+ gtk_widget_get_events (self->window)
+ | GDK_POINTER_MOTION_MASK
+ | GDK_BUTTON_PRESS_MASK
+ | GDK_BUTTON_RELEASE_MASK
+ | GDK_KEY_PRESS_MASK
+ | GDK_KEY_RELEASE_MASK
+ | GDK_EXPOSURE_MASK
+ | GDK_VISIBILITY_NOTIFY_MASK
+ | GDK_ENTER_NOTIFY_MASK
+ | GDK_LEAVE_NOTIFY_MASK);
+
+ context = gtk_widget_get_style_context (self->window);
+ gtk_style_context_remove_class (context, "background");
+ provider = gtk_css_provider_new ();
+ gtk_css_provider_load_from_data (provider, ".backup-active { background-color: black; }", -1, NULL);
+ gtk_style_context_add_provider (context,
+ GTK_STYLE_PROVIDER (provider),
+ GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ gtk_style_context_add_class (context, "backup-active");
+ g_object_unref (provider);
+
+ self->fixed = gtk_fixed_new ();
+ gtk_container_add (GTK_CONTAINER (self->window), self->fixed);
+
+ box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_set_valign (box, GTK_ALIGN_CENTER);
+
+ widget = gtk_image_new_from_icon_name ("cinnamon-symbolic", GTK_ICON_SIZE_DIALOG);
+ gtk_image_set_pixel_size (GTK_IMAGE (widget), 100);
+ gtk_widget_set_halign (widget, GTK_ALIGN_CENTER);
+ gtk_box_pack_start (GTK_BOX (box), widget, FALSE, FALSE, 6);
+ // This is the first line of text for the backup-locker, explaining how to switch to tty
+ // and run 'cinnamon-unlock-desktop' command. This appears if the screensaver crashes.
+ widget = gtk_label_new (_("Something went wrong with Cinnamon."));
+ attrs = pango_attr_list_new ();
+ pango_attr_list_insert (attrs, pango_attr_size_new (20 * PANGO_SCALE));
+ pango_attr_list_insert (attrs, pango_attr_foreground_new (65535, 65535, 65535));
+ gtk_label_set_attributes (GTK_LABEL (widget), attrs);
+ pango_attr_list_unref (attrs);
+ gtk_widget_set_halign (widget, GTK_ALIGN_CENTER);
+ gtk_box_pack_start (GTK_BOX (box), widget, FALSE, FALSE, 6);
+
+ self->stack = gtk_stack_new ();
+ gtk_stack_set_homogeneous (GTK_STACK (self->stack), TRUE);
+ gtk_stack_set_transition_type (GTK_STACK (self->stack), GTK_STACK_TRANSITION_TYPE_CROSSFADE);
+ gtk_box_pack_start (GTK_BOX (box), self->stack, FALSE, FALSE, 6);
+
+ // "auto" page: recovery button
+ self->recover_button = gtk_button_new_with_label (_("Attempt to restart"));
+ gtk_widget_set_halign (self->recover_button, GTK_ALIGN_CENTER);
+ gtk_widget_set_valign (self->recover_button, GTK_ALIGN_CENTER);
+ gtk_style_context_add_class (gtk_widget_get_style_context (self->recover_button), GTK_STYLE_CLASS_SUGGESTED_ACTION);
+ g_signal_connect (self->recover_button, "clicked", G_CALLBACK (on_recover_clicked), self);
+ gtk_stack_add_named (GTK_STACK (self->stack), self->recover_button, "auto");
+
+ // "manual" page: subtitle, TTY instructions, bug report
+ {
+ GtkWidget *manual_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
+
+ widget = gtk_label_new (_("We'll help you get your desktop back"));
+ attrs = pango_attr_list_new ();
+ pango_attr_list_insert (attrs, pango_attr_size_new (12 * PANGO_SCALE));
+ pango_attr_list_insert (attrs, pango_attr_foreground_new (65535, 65535, 65535));
+ gtk_label_set_attributes (GTK_LABEL (widget), attrs);
+ pango_attr_list_unref (attrs);
+ gtk_widget_set_halign (widget, GTK_ALIGN_CENTER);
+ gtk_box_pack_start (GTK_BOX (manual_box), widget, FALSE, FALSE, 6);
+
+ const gchar *steps[] = {
+ // Bulleted list of steps to take to unlock the desktop
+ N_("Switch to a console using ."),
+ N_("Log in by typing your user name followed by your password."),
+ N_("At the prompt, type 'cinnamon-unlock-desktop' and press Enter."),
+ N_("Switch back to your unlocked desktop using .")
+ };
+
+ const gchar *bug_report[] = {
+ N_("If you can reproduce this behavior, please file a report here:"),
+ "https://github.com/linuxmint/cinnamon"
+ };
+
+ GString *str = g_string_new (NULL);
+ gchar *tmp0 = NULL;
+ gchar *tmp1 = NULL;
+
+ tmp0 = g_strdup_printf (_(steps[0]), self->term_tty);
+ tmp1 = g_strdup_printf ("\xe2\x80\xa2 %s\n", tmp0);
+ g_string_append (str, tmp1);
+ g_free (tmp0);
+ g_free (tmp1);
+ tmp1 = g_strdup_printf ("\xe2\x80\xa2 %s\n", _(steps[1]));
+ g_string_append (str, tmp1);
+ g_free (tmp1);
+ tmp1 = g_strdup_printf ("\xe2\x80\xa2 %s\n", _(steps[2]));
+ g_string_append (str, tmp1);
+ g_free (tmp1);
+ tmp0 = g_strdup_printf (_(steps[3]), self->session_tty);
+ tmp1 = g_strdup_printf ("\xe2\x80\xa2 %s\n", tmp0);
+ g_string_append (str, tmp1);
+ g_free (tmp0);
+ g_free (tmp1);
+
+ g_string_append (str, "\n");
+
+ for (int i = 0; i < G_N_ELEMENTS (bug_report); i++)
+ {
+ gchar *line = g_strdup_printf ("%s\n", _(bug_report[i]));
+ g_string_append (str, line);
+ g_free (line);
+ }
+
+ widget = gtk_label_new (str->str);
+ g_string_free (str, TRUE);
+
+ attrs = pango_attr_list_new ();
+ pango_attr_list_insert (attrs, pango_attr_size_new (10 * PANGO_SCALE));
+ pango_attr_list_insert (attrs, pango_attr_foreground_new (65535, 65535, 65535));
+ gtk_label_set_attributes (GTK_LABEL (widget), attrs);
+ pango_attr_list_unref (attrs);
+ gtk_label_set_line_wrap (GTK_LABEL (widget), TRUE);
+ gtk_widget_set_halign (widget, GTK_ALIGN_CENTER);
+ gtk_box_pack_start (GTK_BOX (manual_box), widget, FALSE, FALSE, 6);
+
+ gtk_widget_show_all (manual_box);
+ gtk_stack_add_named (GTK_STACK (self->stack), manual_box, "manual");
+ }
+
+ gtk_widget_show_all (box);
+ gtk_widget_set_no_show_all (box, TRUE);
+ gtk_widget_hide (box);
+ self->info_box = box;
+
+ g_signal_connect_swapped (self->info_box, "realize", G_CALLBACK (position_info_box), self);
+
+ gtk_fixed_put (GTK_FIXED (self->fixed), self->info_box, 0, 0);
+ gtk_widget_show (self->fixed);
+
+ self->event_filter = cs_gdk_event_filter_new (self->window);
+ g_signal_connect (self->event_filter, "xscreen-size", G_CALLBACK (root_window_size_changed), self);
+ self->grabber = cs_event_grabber_new ();
+
+ g_signal_connect (self->window, "realize", G_CALLBACK (on_window_realize), self);
+
+ g_signal_connect_object (gdk_screen_get_default (), "composited-changed",
+ G_CALLBACK (on_composited_changed), self, G_CONNECT_SWAPPED);
+
+ visual = gdk_screen_get_rgba_visual (gdk_screen_get_default ());
+ if (visual)
+ gtk_widget_set_visual (self->window, visual);
+
+ gtk_widget_realize (self->window);
+ gtk_widget_show (self->window);
+}
+
+static void
+window_monitor_thread (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable)
+{
+ GSubprocess *xprop_proc;
+ GError *error;
+
+ gulong xid = GDK_POINTER_TO_XID (task_data);
+ gchar *xid_str = g_strdup_printf ("0x%lx", xid);
+ error = NULL;
+
+ xprop_proc = g_subprocess_new (G_SUBPROCESS_FLAGS_STDOUT_SILENCE,
+ &error,
+ "xprop",
+ "-spy",
+ "-id", (const gchar *) xid_str,
+ NULL);
+
+ g_free (xid_str);
+
+ if (xprop_proc == NULL)
+ {
+ g_warning ("Unable to monitor window: %s", error->message);
+ }
+ else
+ {
+ g_debug ("xprop monitoring window 0x%lx - waiting", xid);
+ g_subprocess_wait (xprop_proc, cancellable, &error);
+
+ if (error != NULL)
+ {
+ if (error->code != G_IO_ERROR_CANCELLED)
+ {
+ g_warning ("xprop error: %s", error->message);
+ }
+ else
+ {
+ g_debug ("xprop cancelled");
+ }
+ }
+ else
+ {
+ gint exit_status = g_subprocess_get_exit_status (xprop_proc);
+ g_debug ("xprop exited (status=%d)", exit_status);
+ }
+ }
+ g_clear_error (&error);
+ g_task_return_boolean (task, TRUE);
+}
+
+static void
+screensaver_window_gone (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ BackupLocker *self = BACKUP_LOCKER (user_data);
+ GCancellable *task_cancellable = g_task_get_cancellable (G_TASK (result));
+ gulong xid = GDK_POINTER_TO_XID (g_task_get_task_data (G_TASK (result)));
+
+ g_task_propagate_boolean (G_TASK (result), NULL);
+
+ g_debug ("screensaver_window_gone: xid=0x%lx, cancelled=%d",
+ xid, g_cancellable_is_cancelled (task_cancellable));
+
+ if (!g_cancellable_is_cancelled (task_cancellable))
+ {
+ g_mutex_lock (&self->pretty_xid_mutex);
+
+ g_debug ("screensaver_window_gone: xid=0x%lx, pretty_xid=0x%lx, match=%d",
+ xid, self->pretty_xid, xid == self->pretty_xid);
+
+ if (xid == self->pretty_xid)
+ {
+ g_debug ("screensaver_window_gone: ACTIVATING - starting event filter and grabbing");
+ cs_gdk_event_filter_stop (self->event_filter);
+ cs_gdk_event_filter_start (self->event_filter);
+
+ self->should_grab = TRUE;
+ self->pretty_xid = 0;
+ activate_backup_window (self);
+ }
+
+ g_mutex_unlock (&self->pretty_xid_mutex);
+ }
+
+ g_clear_object (&self->monitor_cancellable);
+}
+
+static void
+setup_window_monitor (BackupLocker *self, gulong xid)
+{
+ GTask *task;
+
+ g_debug ("setup_window_monitor: xid=0x%lx", xid);
+
+ g_mutex_lock (&self->pretty_xid_mutex);
+
+ self->should_grab = FALSE;
+ self->pretty_xid = xid;
+
+ self->monitor_cancellable = g_cancellable_new ();
+ task = g_task_new (NULL, self->monitor_cancellable, screensaver_window_gone, self);
+
+ g_task_set_return_on_cancel (task, TRUE);
+ g_task_set_task_data (task, GDK_XID_TO_POINTER (xid), NULL);
+
+ g_task_run_in_thread (task, window_monitor_thread);
+ g_object_unref (task);
+ g_mutex_unlock (&self->pretty_xid_mutex);
+}
+
+static void
+release_grabs_internal (BackupLocker *self)
+{
+ g_clear_handle_id (&self->recover_timeout_id, g_source_remove);
+ g_clear_handle_id (&self->can_restart_check_id, g_source_remove);
+
+ if (!self->should_grab)
+ return;
+
+ g_debug ("release_grabs_internal: releasing grabs, stopping event filter");
+
+ cs_gdk_event_filter_stop (self->event_filter);
+ g_clear_handle_id (&self->activate_idle_id, g_source_remove);
+ ungrab (self);
+
+ if (self->info_box != NULL)
+ gtk_widget_hide (self->info_box);
+}
+
+static void
+handle_lock (BackupLocker *self,
+ GVariant *parameters,
+ GDBusMethodInvocation *invocation)
+{
+ guint64 xid64;
+
+ g_variant_get (parameters, "(tuu)", &xid64, &self->term_tty, &self->session_tty);
+ gulong xid = (gulong) xid64;
+
+ g_debug ("handle_lock: xid=0x%lx, term=%u, session=%u",
+ xid, self->term_tty, self->session_tty);
+
+ if (self->window == NULL)
+ {
+ create_window (self);
+ }
+ else
+ {
+ release_grabs_internal (self);
+
+ if (self->monitor_cancellable != NULL)
+ {
+ g_cancellable_cancel (self->monitor_cancellable);
+ g_clear_object (&self->monitor_cancellable);
+ }
+ }
+
+ if (!self->locked)
+ g_application_hold (G_APPLICATION (self));
+
+ self->locked = TRUE;
+
+ acquire_cow (self);
+ gtk_widget_show (self->window);
+
+ setup_window_monitor (self, xid);
+
+ g_dbus_method_invocation_return_value (invocation, NULL);
+}
+
+static void
+handle_unlock (BackupLocker *self,
+ GDBusMethodInvocation *invocation)
+{
+ g_debug ("handle_unlock");
+
+ if (self->monitor_cancellable != NULL)
+ {
+ g_cancellable_cancel (self->monitor_cancellable);
+ g_clear_object (&self->monitor_cancellable);
+ }
+
+ release_grabs_internal (self);
+ release_cow (self);
+
+ if (self->window != NULL)
+ gtk_widget_hide (self->window);
+
+ g_dbus_method_invocation_return_value (invocation, NULL);
+
+ if (self->locked)
+ {
+ self->locked = FALSE;
+ g_application_release (G_APPLICATION (self));
+ }
+}
+
+static void
+handle_release_grabs (BackupLocker *self,
+ GDBusMethodInvocation *invocation)
+{
+ g_debug ("handle_release_grabs");
+
+ release_grabs_internal (self);
+ release_cow (self);
+
+ g_dbus_method_invocation_return_value (invocation, NULL);
+}
+
+static void
+handle_quit (BackupLocker *self,
+ GDBusMethodInvocation *invocation)
+{
+ g_debug ("handle_quit");
+
+ if (self->monitor_cancellable != NULL)
+ {
+ g_cancellable_cancel (self->monitor_cancellable);
+ g_clear_object (&self->monitor_cancellable);
+ }
+
+ release_grabs_internal (self);
+ release_cow (self);
+
+ if (self->window != NULL)
+ {
+ gtk_widget_destroy (self->window);
+ self->window = NULL;
+ self->info_box = NULL;
+ self->fixed = NULL;
+ self->stack = NULL;
+ self->recover_button = NULL;
+ }
+
+ g_dbus_method_invocation_return_value (invocation, NULL);
+
+ if (self->locked)
+ {
+ self->locked = FALSE;
+ g_application_release (G_APPLICATION (self));
+ }
+}
+
+static void
+handle_method_call (GDBusConnection *connection,
+ const gchar *sender,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ GDBusMethodInvocation *invocation,
+ gpointer user_data)
+{
+ BackupLocker *self = BACKUP_LOCKER (user_data);
+
+ g_debug ("D-Bus method call: %s", method_name);
+
+ if (g_strcmp0 (method_name, "Lock") == 0)
+ handle_lock (self, parameters, invocation);
+ else if (g_strcmp0 (method_name, "Unlock") == 0)
+ handle_unlock (self, invocation);
+ else if (g_strcmp0 (method_name, "ReleaseGrabs") == 0)
+ handle_release_grabs (self, invocation);
+ else if (g_strcmp0 (method_name, "Quit") == 0)
+ handle_quit (self, invocation);
+ else
+ g_dbus_method_invocation_return_error (invocation,
+ G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD,
+ "Unknown method: %s", method_name);
+}
+
+static const GDBusInterfaceVTable interface_vtable =
+{
+ handle_method_call,
+ NULL,
+ NULL,
+};
+
+static gboolean
+sigterm_received (gpointer data)
+{
+ BackupLocker *self = BACKUP_LOCKER (data);
+
+ g_debug ("SIGTERM received, cleaning up");
+
+ if (self->monitor_cancellable != NULL)
+ {
+ g_cancellable_cancel (self->monitor_cancellable);
+ g_clear_object (&self->monitor_cancellable);
+ }
+
+ release_grabs_internal (self);
+ release_cow (self);
+
+ if (self->window != NULL)
+ {
+ gtk_widget_destroy (self->window);
+ self->window = NULL;
+ }
+
+ self->sigterm_src_id = 0;
+ g_application_quit (G_APPLICATION (self));
+
+ return G_SOURCE_REMOVE;
+}
+
+static gboolean
+backup_locker_dbus_register (GApplication *application,
+ GDBusConnection *connection,
+ const gchar *object_path,
+ GError **error)
+{
+ BackupLocker *self = BACKUP_LOCKER (application);
+
+ if (!G_APPLICATION_CLASS (backup_locker_parent_class)->dbus_register (application, connection, object_path, error))
+ return FALSE;
+
+ introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL);
+ g_assert (introspection_data != NULL);
+
+ g_dbus_connection_register_object (connection,
+ BUS_PATH,
+ introspection_data->interfaces[0],
+ &interface_vtable,
+ self,
+ NULL,
+ error);
+
+ if (error != NULL && *error != NULL)
+ {
+ g_critical ("Error registering D-Bus object: %s", (*error)->message);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static void
+backup_locker_dbus_unregister (GApplication *application,
+ GDBusConnection *connection,
+ const gchar *object_path)
+{
+ g_clear_pointer (&introspection_data, g_dbus_node_info_unref);
+
+ G_APPLICATION_CLASS (backup_locker_parent_class)->dbus_unregister (application, connection, object_path);
+}
+
+static void
+backup_locker_startup (GApplication *application)
+{
+ BackupLocker *self = BACKUP_LOCKER (application);
+
+ G_APPLICATION_CLASS (backup_locker_parent_class)->startup (application);
+
+ self->sigterm_src_id = g_unix_signal_add (SIGTERM, (GSourceFunc) sigterm_received, self);
+
+ g_application_hold (application);
+
+ if (!self->hold_mode)
+ g_application_release (application);
+}
+
+static void
+backup_locker_activate (GApplication *application)
+{
+}
+
+static gint
+backup_locker_handle_local_options (GApplication *application,
+ GVariantDict *options)
+{
+ BackupLocker *self = BACKUP_LOCKER (application);
+
+ if (g_variant_dict_contains (options, "version"))
+ {
+ g_print ("%s %s\n", g_get_prgname (), VERSION);
+ return 0;
+ }
+
+ if (g_variant_dict_contains (options, "hold"))
+ self->hold_mode = TRUE;
+
+ return -1;
+}
+
+static void
+backup_locker_init (BackupLocker *self)
+{
+ g_mutex_init (&self->pretty_xid_mutex);
+}
+
+static void
+backup_locker_finalize (GObject *object)
+{
+ BackupLocker *self = BACKUP_LOCKER (object);
+
+ g_clear_handle_id (&self->sigterm_src_id, g_source_remove);
+ g_clear_handle_id (&self->activate_idle_id, g_source_remove);
+ g_clear_handle_id (&self->recover_timeout_id, g_source_remove);
+ g_clear_handle_id (&self->can_restart_check_id, g_source_remove);
+
+ if (self->monitor_cancellable != NULL)
+ {
+ g_cancellable_cancel (self->monitor_cancellable);
+ g_clear_object (&self->monitor_cancellable);
+ }
+
+ if (self->grabber != NULL)
+ {
+ cs_event_grabber_release (self->grabber);
+ g_clear_object (&self->grabber);
+ }
+
+ g_clear_object (&self->event_filter);
+ release_cow (self);
+
+ if (self->window != NULL)
+ {
+ gtk_widget_destroy (self->window);
+ self->window = NULL;
+ }
+
+ g_mutex_clear (&self->pretty_xid_mutex);
+
+ G_OBJECT_CLASS (backup_locker_parent_class)->finalize (object);
+}
+
+static void
+backup_locker_class_init (BackupLockerClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GApplicationClass *app_class = G_APPLICATION_CLASS (klass);
+
+ object_class->finalize = backup_locker_finalize;
+ app_class->dbus_register = backup_locker_dbus_register;
+ app_class->dbus_unregister = backup_locker_dbus_unregister;
+ app_class->startup = backup_locker_startup;
+ app_class->activate = backup_locker_activate;
+ app_class->handle_local_options = backup_locker_handle_local_options;
+}
+
+static void
+update_debug_from_gsettings (void)
+{
+ GSettings *settings = g_settings_new ("org.cinnamon");
+ gboolean debug = g_settings_get_boolean (settings, "debug-screensaver");
+ g_object_unref (settings);
+
+ if (debug)
+ {
+#if (GLIB_CHECK_VERSION(2,80,0))
+ const gchar* const domains[] = { G_LOG_DOMAIN, NULL };
+ g_log_writer_default_set_debug_domains (domains);
+#else
+ g_setenv ("G_MESSAGES_DEBUG", G_LOG_DOMAIN, TRUE);
+#endif
+ }
+}
+
+int
+main (int argc,
+ char **argv)
+{
+ int status;
+ BackupLocker *app;
+
+ const GOptionEntry entries[] = {
+ { "version", 0, 0, G_OPTION_ARG_NONE, NULL, "Version of this application", NULL },
+ { "hold", 0, 0, G_OPTION_ARG_NONE, NULL, "Keep the process running", NULL },
+ { NULL }
+ };
+
+ bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
+ bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+ textdomain (GETTEXT_PACKAGE);
+
+ update_debug_from_gsettings ();
+
+ g_debug ("backup-locker: initializing (pid=%d)", getpid ());
+
+ app = g_object_new (BACKUP_TYPE_LOCKER,
+ "application-id", BUS_NAME,
+ "flags", G_APPLICATION_DEFAULT_FLAGS,
+ "inactivity-timeout", 10000,
+ NULL);
+
+ g_application_add_main_option_entries (G_APPLICATION (app), entries);
+
+ status = g_application_run (G_APPLICATION (app), argc, argv);
+
+ g_debug ("backup-locker: exit");
+
+ g_object_unref (app);
+
+ return status;
+}
diff --git a/src/screensaver/backup-locker/cinnamon-unlock-desktop b/src/screensaver/backup-locker/cinnamon-unlock-desktop
new file mode 100644
index 0000000000..090236fdf0
--- /dev/null
+++ b/src/screensaver/backup-locker/cinnamon-unlock-desktop
@@ -0,0 +1,7 @@
+#!/bin/sh
+# Internal screensaver cleanup
+killall cinnamon-backup-locker 2>/dev/null
+gsettings set org.cinnamon session-locked-state false
+# Legacy cinnamon-screensaver cleanup
+killall cs-backup-locker 2>/dev/null
+killall cinnamon-screensaver 2>/dev/null
diff --git a/src/screensaver/backup-locker/event-grabber.c b/src/screensaver/backup-locker/event-grabber.c
new file mode 100644
index 0000000000..07acbf5548
--- /dev/null
+++ b/src/screensaver/backup-locker/event-grabber.c
@@ -0,0 +1,654 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2004-2006 William Jon McCann
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Suite 500, Boston, MA
+ * 02110-1335, USA.
+ *
+ * Authors: William Jon McCann
+ *
+ */
+
+#include "config.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#ifdef HAVE_XF86MISCSETGRABKEYSSTATE
+# include
+#endif /* HAVE_XF86MISCSETGRABKEYSSTATE */
+
+#include "event-grabber.h"
+
+static void cs_event_grabber_class_init (CsEventGrabberClass *klass);
+static void cs_event_grabber_init (CsEventGrabber *grab);
+static void cs_event_grabber_finalize (GObject *object);
+
+typedef struct
+{
+ GDBusConnection *session_bus;
+
+ guint mouse_hide_cursor : 1;
+ GdkWindow *mouse_grab_window;
+ GdkWindow *keyboard_grab_window;
+ GdkScreen *mouse_grab_screen;
+ GdkScreen *keyboard_grab_screen;
+ xdo_t *xdo;
+
+ GtkWidget *invisible;
+} CsEventGrabberPrivate;
+
+struct _CsEventGrabber
+{
+ GObject parent_instance;
+ CsEventGrabberPrivate *priv;
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (CsEventGrabber, cs_event_grabber, G_TYPE_OBJECT)
+
+static gpointer grab_object = NULL;
+
+static void
+set_net_wm_name (GdkWindow *window,
+ const gchar *name)
+{
+ GdkDisplay *display = gdk_display_get_default ();
+ Window xwindow = gdk_x11_window_get_xid (window);
+
+ gdk_x11_display_error_trap_push (display);
+
+ XChangeProperty (GDK_DISPLAY_XDISPLAY (display), xwindow,
+ gdk_x11_get_xatom_by_name_for_display (display, "_NET_WM_NAME"),
+ gdk_x11_get_xatom_by_name_for_display (display, "UTF8_STRING"), 8,
+ PropModeReplace, (guchar *)name, strlen (name));
+
+ XFlush (GDK_DISPLAY_XDISPLAY (display));
+
+ gdk_x11_display_error_trap_pop_ignored (display);
+}
+
+static const char *
+grab_string (int status)
+{
+ switch (status) {
+ case GDK_GRAB_SUCCESS: return "GrabSuccess";
+ case GDK_GRAB_ALREADY_GRABBED: return "AlreadyGrabbed";
+ case GDK_GRAB_INVALID_TIME: return "GrabInvalidTime";
+ case GDK_GRAB_NOT_VIEWABLE: return "GrabNotViewable";
+ case GDK_GRAB_FROZEN: return "GrabFrozen";
+ default:
+ {
+ static char foo [255];
+ sprintf (foo, "unknown status: %d", status);
+ return foo;
+ }
+ }
+}
+
+#ifdef HAVE_XF86MISCSETGRABKEYSSTATE
+/* This function enables and disables the Ctrl-Alt-KP_star and
+ Ctrl-Alt-KP_slash hot-keys, which (in XFree86 4.2) break any
+ grabs and/or kill the grabbing client. That would effectively
+ unlock the screen, so we don't like that.
+
+ The Ctrl-Alt-KP_star and Ctrl-Alt-KP_slash hot-keys only exist
+ if AllowDeactivateGrabs and/or AllowClosedownGrabs are turned on
+ in XF86Config. I believe they are disabled by default.
+
+ This does not affect any other keys (specifically Ctrl-Alt-BS or
+ Ctrl-Alt-F1) but I wish it did. Maybe it will someday.
+ */
+static void
+xorg_lock_smasher_set_active (CsEventGrabber *grab,
+ gboolean active)
+{
+ int status, event, error;
+
+ if (!XF86MiscQueryExtension (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), &event, &error)) {
+ g_debug ("No XFree86-Misc extension present");
+ return;
+ }
+
+ if (active) {
+ g_debug ("Enabling the x.org grab smasher");
+ } else {
+ g_debug ("Disabling the x.org grab smasher");
+ }
+
+ gdk_error_trap_push ();
+
+ status = XF86MiscSetGrabKeysState (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), active);
+
+ gdk_display_sync (gdk_display_get_default ());
+ error = gdk_error_trap_pop ();
+
+ if (active && status == MiscExtGrabStateAlready) {
+ /* shut up, consider this success */
+ status = MiscExtGrabStateSuccess;
+ }
+
+ if (error == Success) {
+ g_debug ("XF86MiscSetGrabKeysState(%s) returned %s",
+ active ? "on" : "off",
+ (status == MiscExtGrabStateSuccess ? "MiscExtGrabStateSuccess" :
+ status == MiscExtGrabStateLocked ? "MiscExtGrabStateLocked" :
+ status == MiscExtGrabStateAlready ? "MiscExtGrabStateAlready" :
+ "unknown value"));
+ } else {
+ g_debug ("XF86MiscSetGrabKeysState(%s) failed with error code %d",
+ active ? "on" : "off", error);
+ }
+}
+#else
+static void
+xorg_lock_smasher_set_active (CsEventGrabber *grab,
+ gboolean active)
+{
+}
+#endif /* HAVE_XF86MISCSETGRABKEYSSTATE */
+
+static void
+maybe_cancel_ui_grab (CsEventGrabber *grab)
+{
+ if (grab->priv->xdo == NULL)
+ {
+ return;
+ }
+
+ xdo_send_keysequence_window (grab->priv->xdo, CURRENTWINDOW, "Escape", 12000); // 12ms as suggested in xdo.h
+ xdo_send_keysequence_window (grab->priv->xdo, CURRENTWINDOW, "Escape", 12000);
+}
+
+static int
+cs_event_grabber_get_keyboard (CsEventGrabber *grab,
+ GdkWindow *window,
+ GdkScreen *screen)
+{
+ GdkGrabStatus status;
+
+ g_return_val_if_fail (window != NULL, FALSE);
+ g_return_val_if_fail (screen != NULL, FALSE);
+
+ g_debug ("Grabbing keyboard widget=0x%lx", (gulong) GDK_WINDOW_XID (window));
+ status = gdk_keyboard_grab (window, FALSE, GDK_CURRENT_TIME);
+
+ if (status == GDK_GRAB_SUCCESS) {
+ if (grab->priv->keyboard_grab_window != NULL) {
+ g_object_remove_weak_pointer (G_OBJECT (grab->priv->keyboard_grab_window),
+ (gpointer *) &grab->priv->keyboard_grab_window);
+ }
+ grab->priv->keyboard_grab_window = window;
+
+ g_object_add_weak_pointer (G_OBJECT (grab->priv->keyboard_grab_window),
+ (gpointer *) &grab->priv->keyboard_grab_window);
+
+ grab->priv->keyboard_grab_screen = screen;
+ } else {
+ g_debug ("Couldn't grab keyboard! (%s)", grab_string (status));
+ }
+
+ return status;
+}
+
+static int
+cs_event_grabber_get_mouse (CsEventGrabber *grab,
+ GdkWindow *window,
+ GdkScreen *screen,
+ gboolean hide_cursor)
+{
+ GdkGrabStatus status;
+ GdkCursor *cursor;
+
+ g_return_val_if_fail (window != NULL, FALSE);
+ g_return_val_if_fail (screen != NULL, FALSE);
+
+ cursor = gdk_cursor_new (GDK_BLANK_CURSOR);
+
+ g_debug ("Grabbing mouse widget=0x%lx", (gulong) GDK_WINDOW_XID (window));
+ status = gdk_pointer_grab (window, TRUE, 0, NULL,
+ (hide_cursor ? cursor : NULL),
+ GDK_CURRENT_TIME);
+
+ if (status == GDK_GRAB_SUCCESS) {
+ if (grab->priv->mouse_grab_window != NULL) {
+ g_object_remove_weak_pointer (G_OBJECT (grab->priv->mouse_grab_window),
+ (gpointer *) &grab->priv->mouse_grab_window);
+ }
+ grab->priv->mouse_grab_window = window;
+
+ g_object_add_weak_pointer (G_OBJECT (grab->priv->mouse_grab_window),
+ (gpointer *) &grab->priv->mouse_grab_window);
+
+ grab->priv->mouse_grab_screen = screen;
+ grab->priv->mouse_hide_cursor = hide_cursor;
+ }
+
+ g_object_unref (cursor);
+
+ return status;
+}
+
+void
+cs_event_grabber_keyboard_reset (CsEventGrabber *grab)
+{
+ if (grab->priv->keyboard_grab_window != NULL) {
+ g_object_remove_weak_pointer (G_OBJECT (grab->priv->keyboard_grab_window),
+ (gpointer *) &grab->priv->keyboard_grab_window);
+ }
+ grab->priv->keyboard_grab_window = NULL;
+ grab->priv->keyboard_grab_screen = NULL;
+}
+
+static gboolean
+cs_event_grabber_release_keyboard (CsEventGrabber *grab)
+{
+ g_debug ("Ungrabbing keyboard");
+ gdk_keyboard_ungrab (GDK_CURRENT_TIME);
+
+ cs_event_grabber_keyboard_reset (grab);
+
+ return TRUE;
+}
+
+void
+cs_event_grabber_mouse_reset (CsEventGrabber *grab)
+{
+ if (grab->priv->mouse_grab_window != NULL) {
+ g_object_remove_weak_pointer (G_OBJECT (grab->priv->mouse_grab_window),
+ (gpointer *) &grab->priv->mouse_grab_window);
+ }
+
+ grab->priv->mouse_grab_window = NULL;
+ grab->priv->mouse_grab_screen = NULL;
+}
+
+gboolean
+cs_event_grabber_release_mouse (CsEventGrabber *grab)
+{
+ g_debug ("Ungrabbing pointer");
+ gdk_pointer_ungrab (GDK_CURRENT_TIME);
+
+ cs_event_grabber_mouse_reset (grab);
+
+ return TRUE;
+}
+
+static gboolean
+cs_event_grabber_move_mouse (CsEventGrabber *grab,
+ GdkWindow *window,
+ GdkScreen *screen,
+ gboolean hide_cursor)
+{
+ gboolean result;
+ GdkWindow *old_window;
+ GdkScreen *old_screen;
+ gboolean old_hide_cursor;
+
+ /* if the pointer is not grabbed and we have a
+ mouse_grab_window defined then we lost the grab */
+ if (! gdk_pointer_is_grabbed ()) {
+ cs_event_grabber_mouse_reset (grab);
+ }
+
+ if (grab->priv->mouse_grab_window == window) {
+ g_debug ("Window 0x%lx is already grabbed, skipping",
+ (gulong) GDK_WINDOW_XID (grab->priv->mouse_grab_window));
+ return TRUE;
+ }
+
+#if 0
+ g_debug ("Intentionally skipping move pointer grabs");
+ /* FIXME: GTK doesn't like having the pointer grabbed */
+ return TRUE;
+#else
+ if (grab->priv->mouse_grab_window) {
+ g_debug ("Moving pointer grab from 0x%lx to 0x%lx",
+ (gulong) GDK_WINDOW_XID (grab->priv->mouse_grab_window),
+ (gulong) GDK_WINDOW_XID (window));
+ } else {
+ g_debug ("Getting pointer grab on 0x%lx",
+ (gulong) GDK_WINDOW_XID (window));
+ }
+#endif
+
+ g_debug ("*** doing X server grab");
+ gdk_x11_grab_server ();
+
+ old_window = grab->priv->mouse_grab_window;
+ old_screen = grab->priv->mouse_grab_screen;
+ old_hide_cursor = grab->priv->mouse_hide_cursor;
+
+ if (old_window) {
+ cs_event_grabber_release_mouse (grab);
+ }
+
+ result = cs_event_grabber_get_mouse (grab, window, screen, hide_cursor);
+
+ if (result != GDK_GRAB_SUCCESS) {
+ sleep (1);
+ result = cs_event_grabber_get_mouse (grab, window, screen, hide_cursor);
+ }
+
+ if ((result != GDK_GRAB_SUCCESS) && old_window) {
+ g_debug ("Could not grab mouse for new window. Resuming previous grab.");
+ cs_event_grabber_get_mouse (grab, old_window, old_screen, old_hide_cursor);
+ }
+
+ g_debug ("*** releasing X server grab");
+ gdk_x11_ungrab_server ();
+ gdk_flush ();
+
+ return (result == GDK_GRAB_SUCCESS);
+}
+
+static gboolean
+cs_event_grabber_move_keyboard (CsEventGrabber *grab,
+ GdkWindow *window,
+ GdkScreen *screen)
+{
+ gboolean result;
+ GdkWindow *old_window;
+ GdkScreen *old_screen;
+
+ if (grab->priv->keyboard_grab_window == window) {
+ g_debug ("Window 0x%lx is already grabbed, skipping",
+ (gulong) GDK_WINDOW_XID (grab->priv->keyboard_grab_window));
+ return TRUE;
+ }
+
+ if (grab->priv->keyboard_grab_window != NULL) {
+ g_debug ("Moving keyboard grab from 0x%lx to 0x%lx",
+ (gulong) GDK_WINDOW_XID (grab->priv->keyboard_grab_window),
+ (gulong) GDK_WINDOW_XID (window));
+ } else {
+ g_debug ("Getting keyboard grab on 0x%lx",
+ (gulong) GDK_WINDOW_XID (window));
+
+ }
+
+ g_debug ("*** doing X server grab");
+ gdk_x11_grab_server ();
+
+ old_window = grab->priv->keyboard_grab_window;
+ old_screen = grab->priv->keyboard_grab_screen;
+
+ if (old_window) {
+ cs_event_grabber_release_keyboard (grab);
+ }
+
+ result = cs_event_grabber_get_keyboard (grab, window, screen);
+
+ if (result != GDK_GRAB_SUCCESS) {
+ sleep (1);
+ result = cs_event_grabber_get_keyboard (grab, window, screen);
+ }
+
+ if ((result != GDK_GRAB_SUCCESS) && old_window) {
+ g_debug ("Could not grab keyboard for new window. Resuming previous grab.");
+ cs_event_grabber_get_keyboard (grab, old_window, old_screen);
+ }
+
+ g_debug ("*** releasing X server grab");
+ gdk_x11_ungrab_server ();
+ gdk_flush ();
+
+ return (result == GDK_GRAB_SUCCESS);
+}
+
+static void
+cs_event_grabber_nuke_focus (void)
+{
+ Window focus = 0;
+ int rev = 0;
+
+ g_debug ("Nuking focus");
+
+ gdk_error_trap_push ();
+
+ XGetInputFocus (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), &focus, &rev);
+
+ XSetInputFocus (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), None, RevertToNone, CurrentTime);
+
+ gdk_error_trap_pop_ignored ();
+}
+
+void
+cs_event_grabber_release (CsEventGrabber *grab)
+{
+ g_debug ("Releasing all grabs");
+
+ cs_event_grabber_release_mouse (grab);
+ cs_event_grabber_release_keyboard (grab);
+
+ /* FIXME: is it right to enable this ? */
+ xorg_lock_smasher_set_active (grab, TRUE);
+
+ gdk_display_sync (gdk_display_get_default ());
+ gdk_flush ();
+}
+
+gboolean
+cs_event_grabber_grab_window (CsEventGrabber *grab,
+ GdkWindow *window,
+ GdkScreen *screen,
+ gboolean hide_cursor)
+{
+ gboolean mstatus = FALSE;
+ gboolean kstatus = FALSE;
+ int i;
+ int retries = 4;
+ gboolean focus_fuckus = FALSE;
+
+ AGAIN:
+
+ for (i = 0; i < retries; i++) {
+ kstatus = cs_event_grabber_get_keyboard (grab, window, screen);
+ if (kstatus == GDK_GRAB_SUCCESS) {
+ break;
+ }
+
+ /* else, wait a second and try to grab again. */
+ sleep (1);
+ }
+
+ if (kstatus != GDK_GRAB_SUCCESS) {
+ if (!focus_fuckus) {
+ focus_fuckus = TRUE;
+ maybe_cancel_ui_grab (grab);
+ cs_event_grabber_nuke_focus ();
+ goto AGAIN;
+ }
+ }
+
+ for (i = 0; i < retries; i++) {
+ mstatus = cs_event_grabber_get_mouse (grab, window, screen, hide_cursor);
+ if (mstatus == GDK_GRAB_SUCCESS) {
+ break;
+ }
+
+ /* else, wait a second and try to grab again. */
+ sleep (1);
+ }
+
+ if (mstatus != GDK_GRAB_SUCCESS) {
+ g_debug ("Couldn't grab pointer! (%s)",
+ grab_string (mstatus));
+ }
+
+#if 0
+ /* FIXME: release the pointer grab so GTK will work */
+ event_grabber_release_mouse (grab);
+#endif
+
+ /* When should we allow blanking to proceed? The current theory
+ is that both a keyboard grab and a mouse grab are mandatory
+
+ - If we don't have a keyboard grab, then we won't be able to
+ read a password to unlock, so the kbd grab is mandatory.
+
+ - If we don't have a mouse grab, then we might not see mouse
+ clicks as a signal to unblank, on-screen widgets won't work ideally,
+ and event_grabber_move_to_window() will spin forever when it gets called.
+ */
+
+ if (kstatus != GDK_GRAB_SUCCESS || mstatus != GDK_GRAB_SUCCESS) {
+ /* Do not blank without a keyboard and mouse grabs. */
+
+ /* Release keyboard or mouse which was grabbed. */
+ if (kstatus == GDK_GRAB_SUCCESS) {
+ cs_event_grabber_release_keyboard (grab);
+ }
+ if (mstatus == GDK_GRAB_SUCCESS) {
+ cs_event_grabber_release_mouse (grab);
+ }
+
+ return FALSE;
+ }
+
+ /* Grab is good, go ahead and blank. */
+ return TRUE;
+}
+
+/* this is used to grab the keyboard and mouse to the root */
+gboolean
+cs_event_grabber_grab_root (CsEventGrabber *grab,
+ gboolean hide_cursor)
+{
+ GdkDisplay *display;
+ GdkWindow *root;
+ GdkScreen *screen;
+ gboolean res;
+
+ g_debug ("Grabbing the root window");
+
+ display = gdk_display_get_default ();
+ gdk_display_get_pointer (display, &screen, NULL, NULL, NULL);
+ root = gdk_screen_get_root_window (screen);
+
+ res = cs_event_grabber_grab_window (grab, root, screen, hide_cursor);
+
+ return res;
+}
+
+/* this is used to grab the keyboard and mouse to an offscreen window */
+gboolean
+cs_event_grabber_grab_offscreen (CsEventGrabber *grab,
+ gboolean hide_cursor)
+{
+ GdkScreen *screen;
+ gboolean res;
+
+ g_debug ("Grabbing an offscreen window");
+
+ screen = gtk_invisible_get_screen (GTK_INVISIBLE (grab->priv->invisible));
+ res = cs_event_grabber_grab_window (grab, gtk_widget_get_window (grab->priv->invisible), screen, hide_cursor);
+
+ return res;
+}
+
+/* This is similar to cs_event_grabber_grab_window but doesn't fail */
+void
+cs_event_grabber_move_to_window (CsEventGrabber *grab,
+ GdkWindow *window,
+ GdkScreen *screen,
+ gboolean hide_cursor)
+{
+ gboolean result = FALSE;
+
+ g_return_if_fail (CS_IS_EVENT_GRABBER (grab));
+
+ xorg_lock_smasher_set_active (grab, FALSE);
+
+ do {
+ result = cs_event_grabber_move_keyboard (grab, window, screen);
+ gdk_flush ();
+ } while (!result);
+
+ do {
+ result = cs_event_grabber_move_mouse (grab, window, screen, hide_cursor);
+ gdk_flush ();
+ } while (!result);
+}
+
+static void
+cs_event_grabber_class_init (CsEventGrabberClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = cs_event_grabber_finalize;
+}
+
+static void
+cs_event_grabber_init (CsEventGrabber *grab)
+{
+ grab->priv = cs_event_grabber_get_instance_private (grab);
+
+ grab->priv->session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL);
+
+ grab->priv->xdo = xdo_new (NULL);
+ if (grab->priv->xdo == NULL)
+ {
+ g_warning ("Xdo context could not be created.");
+ }
+
+ grab->priv->mouse_hide_cursor = FALSE;
+ grab->priv->invisible = gtk_invisible_new ();
+
+ set_net_wm_name (gtk_widget_get_window (grab->priv->invisible),
+ "event-grabber-window");
+
+ gtk_widget_show (grab->priv->invisible);
+}
+
+static void
+cs_event_grabber_finalize (GObject *object)
+{
+ CsEventGrabber *grab;
+
+ g_return_if_fail (object != NULL);
+ g_return_if_fail (CS_IS_EVENT_GRABBER (object));
+
+ grab = CS_EVENT_GRABBER (object);
+
+ g_object_unref (grab->priv->session_bus);
+
+ g_return_if_fail (grab->priv != NULL);
+
+ gtk_widget_destroy (grab->priv->invisible);
+
+ xdo_free (grab->priv->xdo);
+
+ G_OBJECT_CLASS (cs_event_grabber_parent_class)->finalize (object);
+}
+
+CsEventGrabber *
+cs_event_grabber_new (void)
+{
+ if (grab_object) {
+ g_object_ref (grab_object);
+ } else {
+ grab_object = g_object_new (CS_TYPE_EVENT_GRABBER, NULL);
+ g_object_add_weak_pointer (grab_object,
+ (gpointer *) &grab_object);
+ }
+
+ return CS_EVENT_GRABBER (grab_object);
+}
diff --git a/src/screensaver/backup-locker/event-grabber.h b/src/screensaver/backup-locker/event-grabber.h
new file mode 100644
index 0000000000..5fff96061f
--- /dev/null
+++ b/src/screensaver/backup-locker/event-grabber.h
@@ -0,0 +1,59 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2004-2006 William Jon McCann
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * Authors: William Jon McCann
+ *
+ */
+
+#ifndef __CS_EVENT_GRABBER_H
+#define __CS_EVENT_GRABBER_H
+
+#include
+#include
+
+G_BEGIN_DECLS
+
+#define CS_TYPE_EVENT_GRABBER (cs_event_grabber_get_type ())
+G_DECLARE_FINAL_TYPE (CsEventGrabber, cs_event_grabber, CS, EVENT_GRABBER, GObject)
+
+CsEventGrabber * cs_event_grabber_new (void);
+
+void cs_event_grabber_release (CsEventGrabber *grab);
+gboolean cs_event_grabber_release_mouse (CsEventGrabber *grab);
+
+gboolean cs_event_grabber_grab_window (CsEventGrabber *grab,
+ GdkWindow *window,
+ GdkScreen *screen,
+ gboolean hide_cursor);
+
+gboolean cs_event_grabber_grab_root (CsEventGrabber *grab,
+ gboolean hide_cursor);
+gboolean cs_event_grabber_grab_offscreen (CsEventGrabber *grab,
+ gboolean hide_cursor);
+
+void cs_event_grabber_move_to_window (CsEventGrabber *grab,
+ GdkWindow *window,
+ GdkScreen *screen,
+ gboolean hide_cursor);
+
+void cs_event_grabber_mouse_reset (CsEventGrabber *grab);
+void cs_event_grabber_keyboard_reset (CsEventGrabber *grab);
+
+G_END_DECLS
+
+#endif /* __CS_EVENT_GRABBER_H */
diff --git a/src/screensaver/backup-locker/gdk-event-filter-x11.c b/src/screensaver/backup-locker/gdk-event-filter-x11.c
new file mode 100644
index 0000000000..49ca074d9c
--- /dev/null
+++ b/src/screensaver/backup-locker/gdk-event-filter-x11.c
@@ -0,0 +1,279 @@
+/*
+ * CsGdkEventFilter: Establishes an X event trap for the backup-locker.
+ * It watches for MapNotify/ConfigureNotify events that indicate other
+ * windows appearing above us, and raises the backup-locker window to
+ * stay on top. This handles override-redirect windows (native popups,
+ * notifications, etc.) that could otherwise obscure the locker.
+ */
+
+#include "config.h"
+#include "gdk-event-filter.h"
+
+#include
+#include
+
+#ifdef HAVE_SHAPE_EXT
+#include
+#endif
+#include
+#include
+
+enum {
+ XSCREEN_SIZE,
+ LAST_SIGNAL
+};
+
+static guint signals [LAST_SIGNAL] = { 0, };
+
+struct _CsGdkEventFilter
+{
+ GObject parent_instance;
+
+ GdkDisplay *display;
+ GtkWidget *managed_window;
+ gulong my_xid;
+
+ int shape_event_base;
+};
+
+G_DEFINE_TYPE (CsGdkEventFilter, cs_gdk_event_filter, G_TYPE_OBJECT)
+
+static gchar *
+get_net_wm_name (gulong xwindow)
+{
+ GdkDisplay *display = gdk_display_get_default ();
+ Atom net_wm_name_atom;
+ Atom type;
+ int format;
+ unsigned long nitems, after;
+ unsigned char *data = NULL;
+ gchar *name = NULL;
+
+ net_wm_name_atom = XInternAtom(GDK_DISPLAY_XDISPLAY (display), "_NET_WM_NAME", False);
+
+ XGetWindowProperty(GDK_DISPLAY_XDISPLAY (display),
+ xwindow,
+ net_wm_name_atom, 0, 256,
+ False, AnyPropertyType,
+ &type, &format, &nitems, &after,
+ &data);
+ if (data) {
+ name = g_strdup((char *) data);
+ XFree(data);
+ }
+
+ return name;
+}
+
+static void
+unshape_window (CsGdkEventFilter *filter)
+{
+ g_return_if_fail (CS_IS_GDK_EVENT_FILTER (filter));
+
+ gdk_window_shape_combine_region (gtk_widget_get_window (GTK_WIDGET (filter->managed_window)),
+ NULL,
+ 0,
+ 0);
+}
+
+static void
+raise_self (CsGdkEventFilter *filter,
+ Window event_window,
+ const gchar *event_type)
+{
+ g_autofree gchar *net_wm_name = NULL;
+
+ gdk_x11_display_error_trap_push (filter->display);
+
+ net_wm_name = get_net_wm_name (event_window);
+
+ if (g_strcmp0 (net_wm_name, "event-grabber-window") == 0)
+ {
+ g_debug ("(Ignoring %s from CsEventGrabber window)", event_type);
+ gdk_x11_display_error_trap_pop_ignored (filter->display);
+ return;
+ }
+
+ g_debug ("Received %s from window '%s' (0x%lx), raising ourselves.",
+ event_type,
+ net_wm_name,
+ event_window);
+
+ XRaiseWindow(GDK_DISPLAY_XDISPLAY (filter->display), filter->my_xid);
+ XFlush (GDK_DISPLAY_XDISPLAY (filter->display));
+
+ gdk_x11_display_error_trap_pop_ignored (filter->display);
+}
+
+static GdkFilterReturn
+cs_gdk_event_filter_xevent (CsGdkEventFilter *filter,
+ GdkXEvent *xevent)
+{
+ XEvent *ev;
+
+ ev = xevent;
+ /* MapNotify is used to tell us when new windows are mapped.
+ ConfigureNofify is used to tell us when windows are raised. */
+ switch (ev->xany.type) {
+ case MapNotify:
+ {
+ XMapEvent *xme = &ev->xmap;
+
+ if (xme->window == filter->my_xid)
+ {
+ break;
+ }
+
+ raise_self (filter, xme->window, "MapNotify");
+ break;
+ }
+ case ConfigureNotify:
+ {
+ XConfigureEvent *xce = &ev->xconfigure;
+
+ if (xce->window == GDK_ROOT_WINDOW ())
+ {
+ g_debug ("ConfigureNotify from root window (0x%lx), screen size may have changed.",
+ xce->window);
+ g_signal_emit (filter, signals[XSCREEN_SIZE], 0);
+ break;
+ }
+
+ if (xce->window == filter->my_xid)
+ {
+ break;
+ }
+
+ raise_self (filter, xce->window, "ConfigureNotify");
+ break;
+ }
+ default:
+ {
+#ifdef HAVE_SHAPE_EXT
+ if (ev->xany.type == (filter->shape_event_base + ShapeNotify)) {
+ g_debug ("ShapeNotify event.");
+ unshape_window (filter);
+ }
+#endif
+ }
+ }
+
+ return GDK_FILTER_CONTINUE;
+}
+
+static void
+select_popup_events (CsGdkEventFilter *filter)
+{
+ XWindowAttributes attr;
+ unsigned long events;
+
+ gdk_x11_display_error_trap_push (filter->display);
+
+ memset (&attr, 0, sizeof (attr));
+ XGetWindowAttributes (GDK_DISPLAY_XDISPLAY (filter->display), GDK_ROOT_WINDOW (), &attr);
+
+ events = SubstructureNotifyMask | attr.your_event_mask;
+ XSelectInput (GDK_DISPLAY_XDISPLAY (filter->display), GDK_ROOT_WINDOW (), events);
+
+ gdk_x11_display_error_trap_pop_ignored (filter->display);
+}
+
+static void
+select_shape_events (CsGdkEventFilter *filter)
+{
+#ifdef HAVE_SHAPE_EXT
+ unsigned long events;
+ int shape_error_base;
+
+ gdk_x11_display_error_trap_push (filter->display);
+
+ if (XShapeQueryExtension (GDK_DISPLAY_XDISPLAY (filter->display), &filter->shape_event_base, &shape_error_base)) {
+ events = ShapeNotifyMask;
+
+ XShapeSelectInput (GDK_DISPLAY_XDISPLAY (filter->display),
+ GDK_WINDOW_XID (gtk_widget_get_window (GTK_WIDGET (filter->managed_window))),
+ events);
+ }
+
+ gdk_x11_display_error_trap_pop_ignored (filter->display);
+#endif
+}
+
+static GdkFilterReturn
+xevent_filter (GdkXEvent *xevent,
+ GdkEvent *event,
+ CsGdkEventFilter *filter)
+{
+ return cs_gdk_event_filter_xevent (filter, xevent);
+}
+
+static void
+cs_gdk_event_filter_init (CsGdkEventFilter *filter)
+{
+ filter->shape_event_base = 0;
+ filter->managed_window = NULL;
+ filter->my_xid = 0;
+}
+
+static void
+cs_gdk_event_filter_finalize (GObject *object)
+{
+ CsGdkEventFilter *filter;
+
+ g_return_if_fail (object != NULL);
+ g_return_if_fail (CS_IS_GDK_EVENT_FILTER (object));
+
+ filter = CS_GDK_EVENT_FILTER (object);
+
+ cs_gdk_event_filter_stop (filter);
+ g_object_unref (filter->managed_window);
+
+ G_OBJECT_CLASS (cs_gdk_event_filter_parent_class)->finalize (object);
+}
+
+static void
+cs_gdk_event_filter_class_init (CsGdkEventFilterClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = cs_gdk_event_filter_finalize;
+
+ signals[XSCREEN_SIZE] = g_signal_new ("xscreen-size",
+ G_TYPE_FROM_CLASS (object_class),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+}
+
+void
+cs_gdk_event_filter_start (CsGdkEventFilter *filter)
+{
+ select_popup_events (filter);
+ select_shape_events (filter);
+
+ filter->my_xid = gdk_x11_window_get_xid (gtk_widget_get_window (GTK_WIDGET (filter->managed_window)));
+
+ g_debug ("Starting event filter for backup-locker - 0x%lx", filter->my_xid);
+ gdk_window_add_filter (NULL, (GdkFilterFunc) xevent_filter, filter);
+}
+
+void
+cs_gdk_event_filter_stop (CsGdkEventFilter *filter)
+{
+ gdk_window_remove_filter (NULL, (GdkFilterFunc) xevent_filter, filter);
+}
+
+CsGdkEventFilter *
+cs_gdk_event_filter_new (GtkWidget *managed_window)
+{
+ CsGdkEventFilter *filter;
+
+ filter = g_object_new (CS_TYPE_GDK_EVENT_FILTER,
+ NULL);
+
+ filter->display = gdk_display_get_default ();
+ filter->managed_window = g_object_ref (managed_window);
+
+ return filter;
+}
diff --git a/src/screensaver/backup-locker/gdk-event-filter.h b/src/screensaver/backup-locker/gdk-event-filter.h
new file mode 100644
index 0000000000..368f248d5a
--- /dev/null
+++ b/src/screensaver/backup-locker/gdk-event-filter.h
@@ -0,0 +1,20 @@
+#ifndef __GDK_EVENT_FILTER_H
+#define __GDK_EVENT_FILTER_H
+
+#include
+#include
+
+G_BEGIN_DECLS
+
+#define CS_TYPE_GDK_EVENT_FILTER (cs_gdk_event_filter_get_type ())
+G_DECLARE_FINAL_TYPE (CsGdkEventFilter, cs_gdk_event_filter, CS, GDK_EVENT_FILTER, GObject)
+
+CsGdkEventFilter *cs_gdk_event_filter_new (GtkWidget *managed_window);
+
+void cs_gdk_event_filter_start (CsGdkEventFilter *filter);
+
+void cs_gdk_event_filter_stop (CsGdkEventFilter *filter);
+
+G_END_DECLS
+
+#endif /* __GDK_EVENT_FILTER_H */
diff --git a/src/screensaver/backup-locker/meson.build b/src/screensaver/backup-locker/meson.build
new file mode 100644
index 0000000000..a76496b3cf
--- /dev/null
+++ b/src/screensaver/backup-locker/meson.build
@@ -0,0 +1,31 @@
+backup_locker_headers = [
+ 'gdk-event-filter.h',
+ 'event-grabber.h',
+]
+
+backup_locker_sources = [
+ 'backup-locker.c',
+ 'gdk-event-filter-x11.c',
+ 'event-grabber.c',
+]
+
+executable(
+ 'cinnamon-backup-locker',
+ backup_locker_sources,
+ c_args: [
+ '-DLOCALEDIR="@0@"'.format(join_paths(prefix, datadir, 'locale')),
+ '-DG_LOG_DOMAIN="cinnamon-backup-locker"',
+ '-fno-lto',
+ ],
+ link_args: ['-fno-lto'],
+ include_directories: include_root,
+ dependencies: [config_h, X11, xcomposite, xext, gtk, glib, gdkx11, xdo],
+ install: true,
+ install_dir: libexecdir,
+)
+
+install_data(
+ 'cinnamon-unlock-desktop',
+ install_dir: bindir,
+ install_mode: 'rwxr-xr-x',
+)
diff --git a/src/screensaver/cinnamon-screensaver-pam-helper.c b/src/screensaver/cinnamon-screensaver-pam-helper.c
new file mode 100644
index 0000000000..a135f475b1
--- /dev/null
+++ b/src/screensaver/cinnamon-screensaver-pam-helper.c
@@ -0,0 +1,579 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2004-2006 William Jon McCann
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Suite 500, Boston, MA
+ * 02110-1335, USA.
+ *
+ * Authors: William Jon McCann
+ *
+ */
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+#include "setuid.h"
+#include "cs-auth.h"
+
+#define MAX_FAILURES 5
+
+#define DEBUG(...) if (debug_mode) g_printerr (__VA_ARGS__)
+
+static GMainLoop *ml = NULL;
+
+static gboolean debug_mode = FALSE;
+
+static guint shutdown_id = 0;
+static gchar *password_ptr = NULL;
+static GMutex password_mutex;
+static GCancellable *stdin_cancellable = NULL;
+
+#define CS_PAM_AUTH_FAILURE "CS_PAM_AUTH_FAILURE\n"
+#define CS_PAM_AUTH_SUCCESS "CS_PAM_AUTH_SUCCESS\n"
+#define CS_PAM_AUTH_CANCELLED "CS_PAM_AUTH_CANCELLED\n"
+#define CS_PAM_AUTH_BUSY_TRUE "CS_PAM_AUTH_BUSY_TRUE\n"
+#define CS_PAM_AUTH_BUSY_FALSE "CS_PAM_AUTH_BUSY_FALSE\n"
+
+#define CS_PAM_AUTH_SET_PROMPT_ "CS_PAM_AUTH_SET_PROMPT_"
+#define CS_PAM_AUTH_SET_INFO_ "CS_PAM_AUTH_SET_INFO_"
+#define CS_PAM_AUTH_SET_ERROR_ "CS_PAM_AUTH_SET_ERROR_"
+
+#define CS_PAM_AUTH_REQUEST_SUBPROCESS_EXIT "CS_PAM_AUTH_REQUEST_SUBPROCESS_EXIT"
+
+static void send_cancelled (void);
+
+static gboolean
+shutdown (void)
+{
+ DEBUG ("cinnamon-screensaver-pam-helper (pid %i): shutting down.\n", getpid ());
+
+ g_clear_handle_id (&shutdown_id, g_source_remove);
+ g_clear_object (&stdin_cancellable);
+ g_clear_pointer (&password_ptr, g_free);
+
+ g_main_loop_quit (ml);
+ return G_SOURCE_REMOVE;
+}
+
+static void
+send_failure (void)
+{
+ if (g_cancellable_is_cancelled (stdin_cancellable))
+ {
+ return;
+ }
+
+ g_printf (CS_PAM_AUTH_FAILURE);
+ fflush (stdout);
+}
+
+static void
+send_success (void)
+{
+ if (g_cancellable_is_cancelled (stdin_cancellable))
+ {
+ return;
+ }
+
+ g_printf (CS_PAM_AUTH_SUCCESS);
+ fflush (stdout);
+}
+
+static void
+send_cancelled (void)
+{
+ if (g_cancellable_is_cancelled (stdin_cancellable))
+ {
+ return;
+ }
+
+ g_printf (CS_PAM_AUTH_CANCELLED);
+ fflush (stdout);
+}
+
+static void
+send_busy (gboolean busy)
+{
+ if (g_cancellable_is_cancelled (stdin_cancellable))
+ {
+ return;
+ }
+
+ if (busy)
+ {
+ g_printf (CS_PAM_AUTH_BUSY_TRUE);
+ }
+ else
+ {
+ g_printf (CS_PAM_AUTH_BUSY_FALSE);
+ }
+
+ fflush (stdout);
+}
+
+static void
+send_prompt (const gchar *msg)
+{
+ if (g_cancellable_is_cancelled (stdin_cancellable))
+ {
+ return;
+ }
+
+ g_printf (CS_PAM_AUTH_SET_PROMPT_ "%s_\n", msg);
+ fflush (stdout);
+}
+
+static void
+send_info (const gchar *msg)
+{
+ if (g_cancellable_is_cancelled (stdin_cancellable))
+ {
+ return;
+ }
+
+ g_printf (CS_PAM_AUTH_SET_INFO_ "%s_\n", msg);
+ fflush (stdout);
+}
+
+static void
+send_error (const gchar *msg)
+{
+ if (g_cancellable_is_cancelled (stdin_cancellable))
+ {
+ return;
+ }
+
+ g_printf (CS_PAM_AUTH_SET_ERROR_ "%s_\n", msg);
+ fflush (stdout);
+}
+
+static gboolean
+auth_message_handler (CsAuthMessageStyle style,
+ const char *msg,
+ char **response,
+ gpointer data)
+{
+ gboolean ret;
+
+ DEBUG ("cinnamon-screensaver-pam-helper: Got message style %d: '%s'\n", style, msg);
+ ret = TRUE;
+ *response = NULL;
+
+ switch (style)
+ {
+ case CS_AUTH_MESSAGE_PROMPT_ECHO_ON:
+ DEBUG ("cinnamon-screensaver-pam-helper: CS_AUTH_MESSAGE_PROMPT_ECHO_ON\n");
+ break;
+ case CS_AUTH_MESSAGE_PROMPT_ECHO_OFF:
+ if (msg != NULL)
+ {
+ send_prompt (msg);
+ send_busy (FALSE);
+
+ while (password_ptr == NULL && !g_cancellable_is_cancelled (stdin_cancellable))
+ {
+ g_main_context_iteration (g_main_context_default (), FALSE);
+ usleep (100 * 1000);
+ }
+
+ g_mutex_lock (&password_mutex);
+
+ DEBUG ("cinnamon-screensaver-pam-helper: auth_message_handler processing response string\n");
+
+ if (password_ptr != NULL)
+ {
+ *response = g_strdup (password_ptr);
+ memset (password_ptr, '\b', strlen (password_ptr));
+ g_clear_pointer (&password_ptr, g_free);
+ }
+
+ g_mutex_unlock (&password_mutex);
+ }
+ break;
+ case CS_AUTH_MESSAGE_ERROR_MSG:
+ DEBUG ("CS_AUTH_MESSAGE_ERROR_MSG\n");
+
+ if (msg != NULL)
+ {
+ send_error (msg);
+ }
+ break;
+ case CS_AUTH_MESSAGE_TEXT_INFO:
+ DEBUG ("CS_AUTH_MESSAGE_TEXT_INFO\n");
+
+ if (msg != NULL)
+ {
+ send_info (msg);
+ }
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+
+ if (style == CS_AUTH_MESSAGE_PROMPT_ECHO_OFF)
+ {
+ if (*response == NULL) {
+ DEBUG ("cinnamon-screensaver-pam-helper: Got no response to prompt\n");
+ ret = FALSE;
+ } else {
+ send_busy (TRUE);
+ }
+ }
+
+ /* we may have pending events that should be processed before continuing back into PAM */
+ while (g_main_context_pending (g_main_context_default ()))
+ {
+ g_main_context_iteration(g_main_context_default (), TRUE);
+ }
+
+ return ret;
+}
+
+static gboolean
+do_auth_check (void)
+{
+ GError *error;
+ gboolean res;
+
+ error = NULL;
+
+ res = cs_auth_verify_user (g_get_user_name (),
+ g_getenv ("DISPLAY"),
+ auth_message_handler,
+ NULL,
+ &error);
+
+ DEBUG ("cinnamon-screensaver-pam-helper: Verify user returned: %s\n", res ? "TRUE" : "FALSE");
+
+ if (!res)
+ {
+ if (error != NULL && !g_cancellable_is_cancelled (stdin_cancellable))
+ {
+ DEBUG ("cinnamon-screensaver-pam-helper: Verify user returned error: %s\n", error->message);
+ send_error (error->message);
+ g_error_free (error);
+ }
+ }
+
+ return res;
+}
+
+static gboolean
+auth_check_idle (gpointer user_data)
+{
+ gboolean res;
+ gboolean again;
+ static guint loop_counter = 0;
+
+ again = TRUE;
+ res = do_auth_check ();
+
+ if (res)
+ {
+ again = FALSE;
+ send_success ();
+ }
+ else
+ {
+ loop_counter++;
+
+ if (loop_counter < MAX_FAILURES)
+ {
+ send_failure ();
+ DEBUG ("cinnamon-screensaver-pam-helper: Authentication failed, retrying (%u)\n", loop_counter);
+ }
+ else
+ {
+ DEBUG ("cinnamon-screensaver-pam-helper: Authentication failed, quitting (max failures)\n");
+ again = FALSE;
+ send_cancelled ();
+ }
+ }
+
+ if (again)
+ {
+ return G_SOURCE_CONTINUE;
+ }
+
+ g_cancellable_cancel (stdin_cancellable);
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+stdin_monitor_task_thread (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable)
+{
+
+ GInputStream *stream = G_INPUT_STREAM (g_unix_input_stream_new (STDIN_FILENO, FALSE));
+ gssize size;
+ GError *error =NULL;
+
+ while (!g_cancellable_is_cancelled (cancellable))
+ {
+ guint8 input[256];
+ memset (input, 0, sizeof (input));
+ // Blocks
+ size = g_input_stream_read (stream, input, 255, cancellable, &error);
+
+ if (error)
+ {
+ g_cancellable_cancel (cancellable);
+ break;
+ }
+
+ if (size == 0)
+ {
+ g_cancellable_cancel (cancellable);
+ break;
+ }
+
+ g_mutex_lock (&password_mutex);
+
+ if (size > 0)
+ {
+ if (input [size - 1] == '\n')
+ {
+ input [size - 1] = 0;
+ }
+
+ password_ptr = g_strdup ((gchar *) input);
+ memset (input, '\b', sizeof (input));
+ }
+
+ g_mutex_unlock (&password_mutex);
+
+ g_usleep (1000);
+ }
+
+ g_object_unref (stream);
+
+ if (error != NULL)
+ {
+ g_task_return_error (task, error);
+ }
+}
+
+static void
+stdin_monitor_task_finished (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GError *error = NULL;
+ g_task_propagate_boolean (G_TASK (result), &error);
+
+ if (error != NULL)
+ {
+ if (error->code != G_IO_ERROR_CANCELLED)
+ {
+ g_critical ("cinnamon-screensaver-pam-helper: stdin monitor: Could not read input from cinnamon-screensaver: %s", error->message);
+ }
+ g_error_free (error);
+ }
+
+ DEBUG ("cinnamon-screensaver-pam-helper: stdin_monitor_task_finished (Cancelled: %d)\n",
+ g_cancellable_is_cancelled (stdin_cancellable));
+
+ shutdown ();
+}
+
+static void
+setup_stdin_monitor (void)
+{
+ GTask *task;
+
+ stdin_cancellable = g_cancellable_new ();
+ task = g_task_new (NULL, stdin_cancellable, stdin_monitor_task_finished, NULL);
+
+ g_task_run_in_thread (task, stdin_monitor_task_thread);
+ g_object_unref (task);
+}
+
+
+/*
+ * Copyright (c) 1991-2004 Jamie Zawinski
+ * Copyright (c) 2005 William Jon McCann
+ *
+ * Initializations that potentially take place as a privileged user:
+ If the executable is setuid root, then these initializations
+ are run as root, before discarding privileges.
+*/
+static gboolean
+privileged_initialization (int *argc,
+ char **argv,
+ gboolean verbose)
+{
+ gboolean ret;
+ char *nolock_reason;
+ char *orig_uid;
+ char *uid_message;
+
+#ifndef NO_LOCKING
+ /* before hack_uid () for proper permissions */
+ cs_auth_priv_init ();
+#endif /* NO_LOCKING */
+
+ ret = hack_uid (&nolock_reason,
+ &orig_uid,
+ &uid_message);
+
+ if (nolock_reason)
+ {
+ DEBUG ("cinnamon-screensaver-pam-helper: Locking disabled: %s\n", nolock_reason);
+ }
+
+ if (uid_message && verbose)
+ {
+ g_print ("cinnamon-screensaver-pam-helper: Modified UID: %s", uid_message);
+ }
+
+ g_free (nolock_reason);
+ g_free (orig_uid);
+ g_free (uid_message);
+
+ return ret;
+}
+
+
+/*
+ * Copyright (c) 1991-2004 Jamie Zawinski
+ * Copyright (c) 2005 William Jon McCann
+ *
+ * Figure out what locking mechanisms are supported.
+ */
+static gboolean
+lock_initialization (int *argc,
+ char **argv,
+ char **nolock_reason,
+ gboolean verbose)
+{
+ if (nolock_reason != NULL)
+ {
+ *nolock_reason = NULL;
+ }
+
+#ifdef NO_LOCKING
+ if (nolock_reason != NULL)
+ {
+ *nolock_reason = g_strdup ("not compiled with locking support");
+ }
+
+ return FALSE;
+#else /* !NO_LOCKING */
+
+ /* Finish initializing locking, now that we're out of privileged code. */
+ if (!cs_auth_init ())
+ {
+ if (nolock_reason != NULL)
+ {
+ *nolock_reason = g_strdup ("error getting password");
+ }
+
+ return FALSE;
+ }
+
+#endif /* NO_LOCKING */
+
+ return TRUE;
+}
+
+static void
+response_lock_init_failed (void)
+{
+ /* if we fail to lock then we should drop the dialog */
+ send_success ();
+}
+
+static gboolean
+handle_sigterm (gpointer data)
+{
+ DEBUG ("cinnamon-screensaver-pam-helper (pid %i): SIGTERM, shutting down\n", getpid ());
+
+ g_cancellable_cancel (stdin_cancellable);
+ return G_SOURCE_REMOVE;
+}
+
+int
+main (int argc,
+ char **argv)
+{
+ GOptionContext *context;
+ GError *error = NULL;
+ char *nolock_reason = NULL;
+
+ g_unix_signal_add (SIGTERM, (GSourceFunc) handle_sigterm, NULL);
+
+ bindtextdomain (GETTEXT_PACKAGE, "/usr/share/locale");
+
+ if (! privileged_initialization (&argc, argv, debug_mode))
+ {
+ response_lock_init_failed ();
+ exit (1);
+ }
+
+ static GOptionEntry entries [] = {
+ { "debug", 0, 0, G_OPTION_ARG_NONE, &debug_mode,
+ N_("Show debugging output"), NULL },
+ { NULL }
+ };
+
+ context = g_option_context_new (N_("\n\nPAM interface for cinnamon-screensaver."));
+ g_option_context_set_translation_domain (context, GETTEXT_PACKAGE);
+ g_option_context_add_main_entries (context, entries, GETTEXT_PACKAGE);
+
+ if (!g_option_context_parse (context, &argc, &argv, &error)) {
+ g_critical ("Failed to parse arguments: %s", error->message);
+ g_error_free (error);
+ g_option_context_free (context);
+ exit (1);
+ }
+
+ g_option_context_free (context);
+
+ if (! lock_initialization (&argc, argv, &nolock_reason, debug_mode))
+ {
+ if (nolock_reason != NULL)
+ {
+ DEBUG ("cinnamon-screensaver-pam-helper: Screen locking disabled: %s\n", nolock_reason);
+ g_free (nolock_reason);
+ }
+ response_lock_init_failed ();
+
+ exit (1);
+ }
+
+ cs_auth_set_verbose (debug_mode);
+ DEBUG ("cinnamon-screensaver-pam-helper (pid %i): start\n", getpid ());
+
+ setup_stdin_monitor ();
+ g_idle_add ((GSourceFunc) auth_check_idle, NULL);
+
+ ml = g_main_loop_new (NULL, FALSE);
+ g_main_loop_run (ml);
+
+ DEBUG ("cinnamon-screensaver-pam-helper: exit\n");
+ return 0;
+}
diff --git a/src/screensaver/cs-auth-pam.c b/src/screensaver/cs-auth-pam.c
new file mode 100644
index 0000000000..c74af6c67d
--- /dev/null
+++ b/src/screensaver/cs-auth-pam.c
@@ -0,0 +1,789 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2006 William Jon McCann
+ * Copyright (C) 2006 Ray Strode
+ * Copyright (C) 2003 Bill Nottingham
+ * Copyright (c) 1993-2003 Jamie Zawinski
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Suite 500, Boston, MA
+ * 02110-1335, USA.
+ *
+ */
+
+#include "config.h"
+
+#include
+#ifdef HAVE_UNISTD_H
+# include
+#endif
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#include "cs-auth.h"
+
+#include "subprocs.h"
+
+/* Some time between Red Hat 4.2 and 7.0, the words were transposed
+ in the various PAM_x_CRED macro names. Yay!
+*/
+#ifndef PAM_REFRESH_CRED
+# define PAM_REFRESH_CRED PAM_CRED_REFRESH
+#endif
+
+#ifdef HAVE_PAM_FAIL_DELAY
+/* We handle delays ourself.*/
+/* Don't set this to 0 (Linux bug workaround.) */
+# define PAM_NO_DELAY(pamh) pam_fail_delay ((pamh), 1)
+#else /* !HAVE_PAM_FAIL_DELAY */
+# define PAM_NO_DELAY(pamh) /* */
+#endif /* !HAVE_PAM_FAIL_DELAY */
+
+
+/* On SunOS 5.6, and on Linux with PAM 0.64, pam_strerror() takes two args.
+ On some other Linux systems with some other version of PAM (e.g.,
+ whichever Debian release comes with a 2.2.5 kernel) it takes one arg.
+ I can't tell which is more "recent" or "correct" behavior, so configure
+ figures out which is in use for us. Shoot me!
+*/
+#ifdef PAM_STRERROR_TWO_ARGS
+# define PAM_STRERROR(pamh, status) pam_strerror((pamh), (status))
+#else /* !PAM_STRERROR_TWO_ARGS */
+# define PAM_STRERROR(pamh, status) pam_strerror((status))
+#endif /* !PAM_STRERROR_TWO_ARGS */
+
+static GMainLoop *auth_loop = NULL;
+static gboolean verbose_enabled = FALSE;
+static pam_handle_t *pam_handle = NULL;
+static gboolean did_we_ask_for_password = FALSE;
+
+#define DEBUG(...) if (verbose_enabled) g_printerr (__VA_ARGS__)
+
+struct pam_closure {
+ const char *username;
+ CsAuthMessageFunc cb_func;
+ gpointer cb_data;
+ int signal_fd;
+ int result;
+};
+
+typedef struct {
+ struct pam_closure *closure;
+ CsAuthMessageStyle style;
+ const char *msg;
+ char **resp;
+ gboolean should_interrupt_stack;
+} GsAuthMessageHandlerData;
+
+static GCond message_handled_condition;
+static GMutex message_handler_mutex;
+
+GQuark
+cs_auth_error_quark (void)
+{
+ static GQuark quark = 0;
+ if (! quark) {
+ quark = g_quark_from_static_string ("cs_auth_error");
+ }
+
+ return quark;
+}
+
+void
+cs_auth_set_verbose (gboolean enabled)
+{
+ verbose_enabled = enabled;
+}
+
+gboolean
+cs_auth_get_verbose (void)
+{
+ return verbose_enabled;
+}
+
+static CsAuthMessageStyle
+pam_style_to_cs_style (int pam_style)
+{
+ CsAuthMessageStyle style;
+
+ switch (pam_style) {
+ case PAM_PROMPT_ECHO_ON:
+ style = CS_AUTH_MESSAGE_PROMPT_ECHO_ON;
+ break;
+ case PAM_PROMPT_ECHO_OFF:
+ style = CS_AUTH_MESSAGE_PROMPT_ECHO_OFF;
+ break;
+ case PAM_ERROR_MSG:
+ style = CS_AUTH_MESSAGE_ERROR_MSG;
+ break;
+ case PAM_TEXT_INFO:
+ style = CS_AUTH_MESSAGE_TEXT_INFO;
+ break;
+ default:
+ g_assert_not_reached ();
+ break;
+ }
+
+ return style;
+}
+
+static gboolean
+auth_message_handler (CsAuthMessageStyle style,
+ const char *msg,
+ char **response,
+ gpointer data)
+{
+ gboolean ret;
+
+ ret = TRUE;
+ *response = NULL;
+
+ switch (style) {
+ case CS_AUTH_MESSAGE_PROMPT_ECHO_ON:
+ break;
+ case CS_AUTH_MESSAGE_PROMPT_ECHO_OFF:
+ if (msg != NULL && g_str_has_prefix (msg, _("Password:"))) {
+ did_we_ask_for_password = TRUE;
+ }
+ break;
+ case CS_AUTH_MESSAGE_ERROR_MSG:
+ break;
+ case CS_AUTH_MESSAGE_TEXT_INFO:
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+
+ return ret;
+}
+
+static gboolean
+cs_auth_queued_message_handler (GsAuthMessageHandlerData *data)
+{
+ gboolean res;
+
+ if (cs_auth_get_verbose ()) {
+ DEBUG ("Waiting for lock\n");
+ }
+
+ g_mutex_lock (&message_handler_mutex);
+
+ if (cs_auth_get_verbose ()) {
+ DEBUG ("Waiting for response\n");
+ }
+
+ res = data->closure->cb_func (data->style,
+ data->msg,
+ data->resp,
+ data->closure->cb_data);
+ data->should_interrupt_stack = res == FALSE;
+
+ DEBUG ("should interrupt: %d\n", data->should_interrupt_stack);
+
+ g_cond_signal (&message_handled_condition);
+ g_mutex_unlock (&message_handler_mutex);
+
+ if (cs_auth_get_verbose ()) {
+ DEBUG ("Got response\n");
+ }
+
+ return FALSE;
+}
+
+static gboolean
+cs_auth_run_message_handler (struct pam_closure *c,
+ CsAuthMessageStyle style,
+ const char *msg,
+ char **resp)
+{
+ GsAuthMessageHandlerData data;
+
+ data.closure = c;
+ data.style = style;
+ data.msg = msg;
+ data.resp = resp;
+ data.should_interrupt_stack = TRUE;
+
+ g_mutex_lock (&message_handler_mutex);
+
+ /* Queue the callback in the gui (the main) thread
+ */
+ g_idle_add ((GSourceFunc) cs_auth_queued_message_handler, &data);
+
+ if (cs_auth_get_verbose ()) {
+ DEBUG ("cs-auth-pam (pid %i): Waiting for response to message style %d: '%s'\n", getpid (), style, msg);
+ }
+
+ /* Wait for the response
+ */
+ g_cond_wait (&message_handled_condition,
+ &message_handler_mutex);
+ g_mutex_unlock (&message_handler_mutex);
+
+ if (cs_auth_get_verbose ()) {
+ DEBUG ("cs-auth-pam (pid %i): Got response to message style %d: interrupt:%d\n", getpid (), style, data.should_interrupt_stack);
+ }
+
+ return data.should_interrupt_stack == FALSE;
+}
+
+static int
+pam_conversation (int nmsgs,
+ const struct pam_message **msg,
+ struct pam_response **resp,
+ void *closure)
+{
+ int replies = 0;
+ struct pam_response *reply = NULL;
+ struct pam_closure *c = (struct pam_closure *) closure;
+ gboolean res;
+ int ret;
+
+ reply = (struct pam_response *) calloc (nmsgs, sizeof (*reply));
+
+ if (reply == NULL) {
+ return PAM_CONV_ERR;
+ }
+
+ res = TRUE;
+ ret = PAM_SUCCESS;
+
+ for (replies = 0; replies < nmsgs && ret == PAM_SUCCESS; replies++) {
+ CsAuthMessageStyle style;
+ char *utf8_msg;
+
+ style = pam_style_to_cs_style (msg [replies]->msg_style);
+
+ utf8_msg = g_locale_to_utf8 (msg [replies]->msg,
+ -1,
+ NULL,
+ NULL,
+ NULL);
+
+ /* if we couldn't convert text from locale then
+ * assume utf-8 and hope for the best */
+ if (utf8_msg == NULL) {
+ char *p;
+ char *q;
+
+ utf8_msg = g_strdup (msg [replies]->msg);
+
+ p = utf8_msg;
+ while (*p != '\0' && !g_utf8_validate ((const char *)p, -1, (const char **)&q)) {
+ *q = '?';
+ p = q + 1;
+ }
+ }
+
+ /* handle message locally first */
+ auth_message_handler (style,
+ utf8_msg,
+ &reply [replies].resp,
+ NULL);
+
+ if (c->cb_func != NULL) {
+ if (cs_auth_get_verbose ()) {
+ DEBUG ("Handling message style %d: '%s'\n", style, utf8_msg);
+ }
+
+ /* blocks until the gui responds
+ */
+ res = cs_auth_run_message_handler (c,
+ style,
+ utf8_msg,
+ &reply [replies].resp);
+
+ if (cs_auth_get_verbose ()) {
+ DEBUG ("Msg handler returned %d\n", res);
+ }
+
+ /* If the handler returns FALSE - interrupt the PAM stack */
+ if (res) {
+ reply [replies].resp_retcode = PAM_SUCCESS;
+ } else {
+ int i;
+ for (i = 0; i <= replies; i++) {
+ free (reply [i].resp);
+ }
+ free (reply);
+ reply = NULL;
+ ret = PAM_CONV_ERR;
+ }
+ }
+
+ g_free (utf8_msg);
+ }
+
+ *resp = reply;
+
+ return ret;
+}
+
+static gboolean
+close_pam_handle (int status)
+{
+
+ if (pam_handle != NULL) {
+ int status2;
+
+ status2 = pam_end (pam_handle, status);
+ pam_handle = NULL;
+
+ if (cs_auth_get_verbose ()) {
+ DEBUG (" pam_end (...) ==> %d (%s)\n",
+ status2,
+ (status2 == PAM_SUCCESS ? "Success" : "Failure"));
+ }
+ }
+
+ g_cond_clear (&message_handled_condition);
+ g_mutex_clear (&message_handler_mutex);
+
+ return TRUE;
+}
+
+static gboolean
+create_pam_handle (const char *username,
+ const char *display,
+ struct pam_conv *conv,
+ int *status_code)
+{
+ int status;
+ const char *service = PAM_SERVICE_NAME;
+ char *disp;
+ gboolean ret;
+
+ if (pam_handle != NULL) {
+ g_warning ("create_pam_handle: Stale pam handle around, cleaning up\n");
+ close_pam_handle (PAM_SUCCESS);
+ }
+
+ /* init things */
+ pam_handle = NULL;
+ status = -1;
+ disp = NULL;
+ ret = TRUE;
+
+ /* Initialize a PAM session for the user */
+ if ((status = pam_start (service, username, conv, &pam_handle)) != PAM_SUCCESS) {
+ pam_handle = NULL;
+ g_warning (_("Unable to establish service %s: %s\n"),
+ service,
+ PAM_STRERROR (NULL, status));
+
+ if (status_code != NULL) {
+ *status_code = status;
+ }
+
+ ret = FALSE;
+ goto out;
+ }
+
+ if (cs_auth_get_verbose ()) {
+ DEBUG ("cs-auth-pam (pid %i): pam_start (\"%s\", \"%s\", ...) ==> %d (%s)\n",
+ getpid (),
+ service,
+ username,
+ status,
+ PAM_STRERROR (pam_handle, status));
+ }
+
+ disp = g_strdup (display);
+ if (disp == NULL) {
+ disp = g_strdup (":0.0");
+ }
+
+ if ((status = pam_set_item (pam_handle, PAM_TTY, disp)) != PAM_SUCCESS) {
+ g_warning (_("Can't set PAM_TTY=%s"), display);
+
+ if (status_code != NULL) {
+ *status_code = status;
+ }
+
+ ret = FALSE;
+ goto out;
+ }
+
+ ret = TRUE;
+ g_cond_init (&message_handled_condition);
+ g_mutex_init (&message_handler_mutex);
+
+ out:
+ if (status_code != NULL) {
+ *status_code = status;
+ }
+
+ g_free (disp);
+
+ return ret;
+}
+
+static void
+set_pam_error (GError **error,
+ int status)
+{
+ if (status == PAM_AUTH_ERR || status == PAM_USER_UNKNOWN) {
+ char *msg;
+
+ if (did_we_ask_for_password) {
+ msg = g_strdup (_("Incorrect password."));
+ } else {
+ msg = g_strdup (_("Authentication failed."));
+ }
+
+ g_set_error (error,
+ CS_AUTH_ERROR,
+ CS_AUTH_ERROR_AUTH_ERROR,
+ "%s",
+ msg);
+ g_free (msg);
+ } else if (status == PAM_PERM_DENIED) {
+ g_set_error (error,
+ CS_AUTH_ERROR,
+ CS_AUTH_ERROR_AUTH_DENIED,
+ "%s",
+ _("Not permitted to gain access at this time."));
+ } else if (status == PAM_ACCT_EXPIRED) {
+ g_set_error (error,
+ CS_AUTH_ERROR,
+ CS_AUTH_ERROR_AUTH_DENIED,
+ "%s",
+ _("No longer permitted to access the system."));
+ } else {
+ g_set_error (error,
+ CS_AUTH_ERROR,
+ CS_AUTH_ERROR_AUTH_ERROR,
+ _("Authentication error: %s"),
+ PAM_STRERROR (NULL, status));
+ }
+
+}
+
+static gpointer
+cs_auth_thread_func (gpointer auth_operation_fd_ptr)
+{
+ static const int flags = 0;
+ int status;
+ int status2;
+ struct timespec timeout;
+ sigset_t set;
+ const void *p;
+ int auth_operation_fd = GPOINTER_TO_INT(auth_operation_fd_ptr);
+
+ timeout.tv_sec = 0;
+ timeout.tv_nsec = 1;
+
+ set = block_sigchld ();
+
+ status = pam_authenticate (pam_handle, flags);
+
+ sigtimedwait (&set, NULL, &timeout);
+ unblock_sigchld ();
+
+ if (cs_auth_get_verbose ()) {
+ DEBUG (" pam_authenticate (...) ==> %d (%s)\n",
+ status,
+ PAM_STRERROR (pam_handle, status));
+ }
+
+ if (status != PAM_SUCCESS) {
+ goto done;
+ }
+
+ if ((status = pam_get_item (pam_handle, PAM_USER, &p)) != PAM_SUCCESS) {
+ /* is not really an auth problem, but it will
+ pretty much look as such, it shouldn't really
+ happen */
+ goto done;
+ }
+
+ /* We don't actually care if the account modules fail or succeed,
+ * but we need to run them anyway because certain pam modules
+ * depend on side effects of the account modules getting run.
+ */
+ status2 = pam_acct_mgmt (pam_handle, 0);
+
+ if (cs_auth_get_verbose ()) {
+ DEBUG ("pam_acct_mgmt (...) ==> %d (%s)\n",
+ status2,
+ PAM_STRERROR (pam_handle, status2));
+ }
+
+ /* FIXME: should we handle these? */
+ switch (status2) {
+ case PAM_SUCCESS:
+ break;
+ case PAM_NEW_AUTHTOK_REQD:
+ break;
+ case PAM_AUTHINFO_UNAVAIL:
+ break;
+ case PAM_ACCT_EXPIRED:
+ break;
+ case PAM_PERM_DENIED:
+ break;
+ default :
+ break;
+ }
+
+ /* Each time we successfully authenticate, refresh credentials,
+ for Kerberos/AFS/DCE/etc. If this fails, just ignore that
+ failure and blunder along; it shouldn't matter.
+
+ Note: this used to be PAM_REFRESH_CRED instead of
+ PAM_REINITIALIZE_CRED, but Jason Heiss
+ says that the Linux PAM library ignores that one, and only refreshes
+ credentials when using PAM_REINITIALIZE_CRED.
+ */
+ status2 = pam_setcred (pam_handle, PAM_REINITIALIZE_CRED);
+ if (cs_auth_get_verbose ()) {
+ DEBUG (" pam_setcred (...) ==> %d (%s)\n",
+ status2,
+ PAM_STRERROR (pam_handle, status2));
+ }
+
+ done:
+ /* we're done, close the fd and wake up the main
+ * loop
+ */
+ close (auth_operation_fd);
+
+ return GINT_TO_POINTER(status);
+}
+
+static gboolean
+cs_auth_loop_quit (GIOChannel *source,
+ GIOCondition condition,
+ gboolean *thread_done)
+{
+ *thread_done = TRUE;
+ g_main_loop_quit (auth_loop);
+ return FALSE;
+}
+
+static gboolean
+cs_auth_pam_verify_user (pam_handle_t *handle,
+ int *status)
+{
+ GThread *auth_thread;
+ GIOChannel *channel;
+ guint watch_id;
+ int auth_operation_fds[2];
+ int auth_status;
+ gboolean thread_done;
+
+ channel = NULL;
+ watch_id = 0;
+ auth_status = PAM_AUTH_ERR;
+
+ /* This pipe gives us a set of fds we can hook into
+ * the event loop to be notified when our helper thread
+ * is ready to be reaped.
+ */
+ if (pipe (auth_operation_fds) < 0) {
+ goto out;
+ }
+
+ if (fcntl (auth_operation_fds[0], F_SETFD, FD_CLOEXEC) < 0) {
+ close (auth_operation_fds[0]);
+ close (auth_operation_fds[1]);
+ goto out;
+ }
+
+ if (fcntl (auth_operation_fds[1], F_SETFD, FD_CLOEXEC) < 0) {
+ close (auth_operation_fds[0]);
+ close (auth_operation_fds[1]);
+ goto out;
+ }
+
+ channel = g_io_channel_unix_new (auth_operation_fds[0]);
+
+ /* we use a recursive main loop to process ui events
+ * while we wait on a thread to handle the blocking parts
+ * of pam authentication.
+ */
+ thread_done = FALSE;
+ watch_id = g_io_add_watch (channel, G_IO_ERR | G_IO_HUP,
+ (GIOFunc) cs_auth_loop_quit, &thread_done);
+
+ auth_thread = g_thread_new ("cs-auth-verify-user",
+ (GThreadFunc) cs_auth_thread_func,
+ GINT_TO_POINTER (auth_operation_fds[1]));
+
+ if (auth_thread == NULL) {
+ goto out;
+ }
+
+ auth_loop = g_main_loop_new (NULL, FALSE);
+ g_main_loop_run (auth_loop);
+ /* if the event loop was quit before the thread is done then we can't
+ * reap the thread without blocking on it finishing. The
+ * thread may not ever finish though if the pam module is blocking.
+ *
+ * The only time the event loop is going to stop when the thread isn't
+ * done, however, is if the dialog quits early (from, e.g., "cancel"),
+ * so we can just exit. An alternative option would be to switch to
+ * using pthreads directly and calling pthread_cancel.
+ */
+ if (!thread_done) {
+ raise (SIGTERM);
+ }
+
+ auth_status = GPOINTER_TO_INT (g_thread_join (auth_thread));
+
+ out:
+ if (watch_id != 0 && !thread_done) {
+ g_source_remove (watch_id);
+ watch_id = 0;
+ }
+
+ if (channel != NULL) {
+ g_io_channel_unref (channel);
+ }
+
+ if (status) {
+ *status = auth_status;
+ }
+
+ return auth_status == PAM_SUCCESS;
+}
+
+/**
+ * cs_auth_verify_user:
+ * @username: user name
+ * @display: display string
+ * @func: (scope async): the auth function callback
+ * @data: (closure func): data for func
+ * @error: Return location for error or %NULL.
+ *
+ * Starts a PAM thread for user authentication.
+ *
+ * Returns: Whether or not the user was authenticated successfully
+ */
+
+gboolean
+cs_auth_verify_user (const char *username,
+ const char *display,
+ CsAuthMessageFunc func,
+ gpointer data,
+ GError **error)
+{
+ int status = -1;
+ struct pam_conv conv;
+ struct pam_closure c;
+ struct passwd *pwent;
+
+ pwent = getpwnam (username);
+ if (pwent == NULL) {
+ return FALSE;
+ }
+
+ c.username = username;
+ c.cb_func = func;
+ c.cb_data = data;
+
+ conv.conv = &pam_conversation;
+ conv.appdata_ptr = (void *) &c;
+
+ /* Initialize PAM. */
+ create_pam_handle (username, display, &conv, &status);
+ if (status != PAM_SUCCESS) {
+ goto done;
+ }
+
+ pam_set_item (pam_handle, PAM_USER_PROMPT, _("Username:"));
+
+ did_we_ask_for_password = FALSE;
+ if (! cs_auth_pam_verify_user (pam_handle, &status)) {
+ goto done;
+ }
+
+ done:
+ if (status != PAM_SUCCESS) {
+ set_pam_error (error, status);
+ }
+
+ close_pam_handle (status);
+
+ return (status == PAM_SUCCESS ? TRUE : FALSE);
+}
+
+gboolean
+cs_auth_init (void)
+{
+ return TRUE;
+}
+
+gboolean
+cs_auth_priv_init (void)
+{
+ /* We have nothing to do at init-time.
+ However, we might as well do some error checking.
+ If "/etc/pam.d" exists and is a directory, but "/etc/pam.d/" PAM_SERVICE_NAME
+ does not exist, warn that PAM probably isn't going to work.
+
+ This is a priv-init instead of a non-priv init in case the directory
+ is unreadable or something (don't know if that actually happens.)
+ */
+ const char dir [] = "/etc/pam.d";
+ const char file [] = "/etc/pam.d/" PAM_SERVICE_NAME;
+ const char file2 [] = "/etc/pam.conf";
+ struct stat st;
+
+ if (g_stat (dir, &st) == 0 && st.st_mode & S_IFDIR) {
+ if (g_stat (file, &st) != 0) {
+ g_warning ("%s does not exist.\n"
+ "Authentication via PAM is unlikely to work.",
+ file);
+ }
+ } else if (g_stat (file2, &st) == 0) {
+ FILE *f = g_fopen (file2, "r");
+ if (f) {
+ gboolean ok = FALSE;
+ char buf[255];
+ while (fgets (buf, sizeof(buf), f)) {
+ if (strstr (buf, PAM_SERVICE_NAME)) {
+ ok = TRUE;
+ break;
+ }
+ }
+
+ fclose (f);
+ if (!ok) {
+ g_warning ("%s does not list the `%s' service.\n"
+ "Authentication via PAM is unlikely to work.",
+ file2, PAM_SERVICE_NAME);
+ }
+ }
+ /* else warn about file2 existing but being unreadable? */
+ } else {
+ g_warning ("Neither %s nor %s exist.\n"
+ "Authentication via PAM is unlikely to work.",
+ file2, file);
+ }
+
+ /* Return true anyway, just in case. */
+ return TRUE;
+}
diff --git a/src/screensaver/cs-auth.h b/src/screensaver/cs-auth.h
new file mode 100644
index 0000000000..5bcb8f0bbe
--- /dev/null
+++ b/src/screensaver/cs-auth.h
@@ -0,0 +1,67 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2006 William Jon McCann
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Suite 500, Boston, MA
+ * 02110-1335, USA.
+ *
+ */
+
+#ifndef __CS_AUTH_H
+#define __CS_AUTH_H
+
+#include
+
+G_BEGIN_DECLS
+
+typedef enum {
+ CS_AUTH_MESSAGE_PROMPT_ECHO_ON,
+ CS_AUTH_MESSAGE_PROMPT_ECHO_OFF,
+ CS_AUTH_MESSAGE_ERROR_MSG,
+ CS_AUTH_MESSAGE_TEXT_INFO
+} CsAuthMessageStyle;
+
+typedef enum {
+ CS_AUTH_ERROR_GENERAL,
+ CS_AUTH_ERROR_AUTH_ERROR,
+ CS_AUTH_ERROR_USER_UNKNOWN,
+ CS_AUTH_ERROR_AUTH_DENIED
+} CsAuthError;
+
+#define PAM_SERVICE_NAME "cinnamon"
+
+typedef gboolean (* CsAuthMessageFunc) (CsAuthMessageStyle style,
+ const char *msg,
+ char **response,
+ gpointer data);
+
+#define CS_AUTH_ERROR cs_auth_error_quark ()
+
+GQuark cs_auth_error_quark (void);
+
+void cs_auth_set_verbose (gboolean verbose);
+gboolean cs_auth_get_verbose (void);
+
+gboolean cs_auth_priv_init (void);
+gboolean cs_auth_init (void);
+gboolean cs_auth_verify_user (const char *username,
+ const char *display,
+ CsAuthMessageFunc func,
+ gpointer data,
+ GError **error);
+
+G_END_DECLS
+
+#endif /* __CS_AUTH_H */
diff --git a/src/screensaver/meson.build b/src/screensaver/meson.build
new file mode 100644
index 0000000000..5ee413de7c
--- /dev/null
+++ b/src/screensaver/meson.build
@@ -0,0 +1,26 @@
+# Screensaver authentication library
+screensaver_sources = [
+ 'cs-auth-pam.c',
+ 'setuid.c',
+ 'subprocs.c',
+]
+
+libcinnamon_screensaver = static_library(
+ 'cinnamon_screensaver',
+ screensaver_sources,
+ include_directories: include_root,
+ dependencies: [glib, gio, gio_unix, pam],
+)
+
+# PAM helper executable
+executable(
+ 'cinnamon-screensaver-pam-helper',
+ 'cinnamon-screensaver-pam-helper.c',
+ link_with: libcinnamon_screensaver,
+ include_directories: include_root,
+ dependencies: [glib, gio, gio_unix],
+ install: true,
+ install_dir: libexecdir,
+)
+
+subdir('backup-locker')
diff --git a/src/screensaver/setuid.c b/src/screensaver/setuid.c
new file mode 100644
index 0000000000..8f39b81b95
--- /dev/null
+++ b/src/screensaver/setuid.c
@@ -0,0 +1,245 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * setuid.c --- management of runtime privileges.
+ *
+ * xscreensaver, Copyright (c) 1993-1998 Jamie Zawinski
+ *
+ * Permission to use, copy, modify, distribute, and sell this software and its
+ * documentation for any purpose is hereby granted without fee, provided that
+ * the above copyright notice appear in all copies and that both that
+ * copyright notice and this permission notice appear in supporting
+ * documentation. No representations are made about the suitability of this
+ * software for any purpose. It is provided "as is" without express or
+ * implied warranty.
+ */
+
+#include "config.h"
+
+#ifdef USE_SETRES
+#define _GNU_SOURCE
+#endif /* USE_SETRES */
+
+#include
+
+#include
+#include
+#include
+#include
+#include /* for getpwnam() and struct passwd */
+#include /* for getgrgid() and struct group */
+
+#include "setuid.h"
+
+static char *
+uid_gid_string (uid_t uid,
+ gid_t gid)
+{
+ static char *buf;
+ struct passwd *p = NULL;
+ struct group *g = NULL;
+
+ p = getpwuid (uid);
+ g = getgrgid (gid);
+
+ buf = g_strdup_printf ("%s/%s (%ld/%ld)",
+ (p && p->pw_name ? p->pw_name : "???"),
+ (g && g->gr_name ? g->gr_name : "???"),
+ (long) uid, (long) gid);
+
+ return buf;
+}
+
+static gboolean
+set_ids_by_number (uid_t uid,
+ gid_t gid,
+ char **message_ret)
+{
+ int uid_errno = 0;
+ int gid_errno = 0;
+ int sgs_errno = 0;
+ struct passwd *p = getpwuid (uid);
+ struct group *g = getgrgid (gid);
+
+ if (message_ret)
+ *message_ret = NULL;
+
+ /* Rumor has it that some implementations of of setuid() do nothing
+ when called with -1; therefore, if the "nobody" user has a uid of
+ -1, then that would be Really Bad. Rumor further has it that such
+ systems really ought to be using -2 for "nobody", since that works.
+ So, if we get a uid (or gid, for good measure) of -1, switch to -2
+ instead. Note that this must be done after we've looked up the
+ user/group names with getpwuid(-1) and/or getgrgid(-1).
+ */
+ if (gid == (gid_t) -1) gid = (gid_t) -2;
+ if (uid == (uid_t) -1) uid = (uid_t) -2;
+
+#ifndef USE_SETRES
+ errno = 0;
+ if (setgroups (1, &gid) < 0)
+ sgs_errno = errno ? errno : -1;
+
+ errno = 0;
+ if (setgid (gid) != 0)
+ gid_errno = errno ? errno : -1;
+
+ errno = 0;
+ if (setuid (uid) != 0)
+ uid_errno = errno ? errno : -1;
+#else /* !USE_SETRES */
+ errno = 0;
+ if (setresgid (gid, gid, gid) != 0)
+ gid_errno = errno ? errno : -1;
+
+ errno = 0;
+ if (setresuid (uid, uid, uid) != 0)
+ uid_errno = errno ? errno : -1;
+#endif /* USE_SETRES */
+
+ if (uid_errno == 0 && gid_errno == 0 && sgs_errno == 0) {
+ static char *reason;
+ reason = g_strdup_printf ("changed uid/gid to %s/%s (%ld/%ld).",
+ (p && p->pw_name ? p->pw_name : "???"),
+ (g && g->gr_name ? g->gr_name : "???"),
+ (long) uid, (long) gid);
+ if (message_ret)
+ *message_ret = g_strdup (reason);
+
+ g_free (reason);
+
+ return TRUE;
+ } else {
+ char *reason = NULL;
+
+ if (sgs_errno) {
+ reason = g_strdup_printf ("couldn't setgroups to %s (%ld)",
+ (g && g->gr_name ? g->gr_name : "???"),
+ (long) gid);
+ if (sgs_errno == -1)
+ fprintf (stderr, "%s: unknown error\n", reason);
+ else {
+ errno = sgs_errno;
+ perror (reason);
+ }
+ g_free (reason);
+ reason = NULL;
+ }
+
+ if (gid_errno) {
+ reason = g_strdup_printf ("couldn't set gid to %s (%ld)",
+ (g && g->gr_name ? g->gr_name : "???"),
+ (long) gid);
+ if (gid_errno == -1)
+ fprintf (stderr, "%s: unknown error\n", reason);
+ else {
+ errno = gid_errno;
+ perror (reason);
+ }
+ g_free (reason);
+ reason = NULL;
+ }
+
+ if (uid_errno) {
+ reason = g_strdup_printf ("couldn't set uid to %s (%ld)",
+ (p && p->pw_name ? p->pw_name : "???"),
+ (long) uid);
+ if (uid_errno == -1)
+ fprintf (stderr, "%s: unknown error\n", reason);
+ else {
+ errno = uid_errno;
+ perror (reason);
+ }
+ g_free (reason);
+ reason = NULL;
+ }
+ return FALSE;
+ }
+ return FALSE;
+}
+
+
+/* If we've been run as setuid or setgid to someone else (most likely root)
+ turn off the extra permissions so that random user-specified programs
+ don't get special privileges. (On some systems it is necessary to install
+ this program as setuid root in order to read the passwd file to implement
+ lock-mode.)
+
+ *** WARNING: DO NOT DISABLE ANY OF THE FOLLOWING CODE!
+ If you do so, you will open a security hole. See the sections
+ of the xscreensaver manual titled "LOCKING AND ROOT LOGINS",
+ and "USING XDM".
+*/
+
+/* Returns TRUE if OK to lock, FALSE otherwise */
+gboolean
+hack_uid (char **nolock_reason,
+ char **orig_uid,
+ char **uid_message)
+{
+ char *reason;
+ gboolean ret;
+
+ ret = TRUE;
+ reason = NULL;
+
+ if (nolock_reason != NULL) {
+ *nolock_reason = NULL;
+ }
+ if (orig_uid != NULL) {
+ *orig_uid = NULL;
+ }
+ if (uid_message != NULL) {
+ *uid_message = NULL;
+ }
+
+ /* Discard privileges, and set the effective user/group ids to the
+ real user/group ids. That is, give up our "chmod +s" rights.
+ */
+ {
+ uid_t euid = geteuid ();
+ gid_t egid = getegid ();
+ uid_t uid = getuid ();
+ gid_t gid = getgid ();
+
+ if (orig_uid != NULL) {
+ *orig_uid = uid_gid_string (euid, egid);
+ }
+
+ if (uid != euid || gid != egid) {
+#ifndef USE_SETRES
+ if (! set_ids_by_number (uid, gid, uid_message)) {
+#else /* !USE_SETRES */
+ if (! set_ids_by_number (euid == 0 ? uid : euid, egid == 0 ? gid : egid, uid_message)) {
+#endif /* USE_SETRES */
+ reason = g_strdup ("unable to discard privileges.");
+
+ ret = FALSE;
+ goto out;
+ }
+ }
+ }
+
+
+ /* Locking can't work when running as root, because we have no way of
+ knowing what the user id of the logged in user is (so we don't know
+ whose password to prompt for.)
+
+ *** WARNING: DO NOT DISABLE THIS CODE!
+ If you do so, you will open a security hole. See the sections
+ of the xscreensaver manual titled "LOCKING AND ROOT LOGINS",
+ and "USING XDM".
+ */
+ if (getuid () == (uid_t) 0) {
+ reason = g_strdup ("running as root");
+ ret = FALSE;
+ goto out;
+ }
+
+ out:
+ if (nolock_reason != NULL) {
+ *nolock_reason = g_strdup (reason);
+ }
+ g_free (reason);
+
+ return ret;
+}
diff --git a/src/screensaver/setuid.h b/src/screensaver/setuid.h
new file mode 100644
index 0000000000..8f5161be58
--- /dev/null
+++ b/src/screensaver/setuid.h
@@ -0,0 +1,27 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * xscreensaver, Copyright (c) 1993-2004 Jamie Zawinski
+ *
+ * Permission to use, copy, modify, distribute, and sell this software and its
+ * documentation for any purpose is hereby granted without fee, provided that
+ * the above copyright notice appear in all copies and that both that
+ * copyright notice and this permission notice appear in supporting
+ * documentation. No representations are made about the suitability of this
+ * software for any purpose. It is provided "as is" without express or
+ * implied warranty.
+ */
+
+#ifndef __GS_SETUID_H
+#define __GS_SETUID_H
+
+#include
+
+G_BEGIN_DECLS
+
+gboolean hack_uid (char **nolock_reason,
+ char **orig_uid,
+ char **uid_message);
+
+G_END_DECLS
+
+#endif /* __GS_SETUID_H */
diff --git a/src/screensaver/subprocs.c b/src/screensaver/subprocs.c
new file mode 100644
index 0000000000..006843798d
--- /dev/null
+++ b/src/screensaver/subprocs.c
@@ -0,0 +1,159 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * subprocs.c --- choosing, spawning, and killing screenhacks.
+ *
+ * xscreensaver, Copyright (c) 1991-2003 Jamie Zawinski
+ * Modified: Copyright (c) 2004 William Jon McCann
+ *
+ * Permission to use, copy, modify, distribute, and sell this software and its
+ * documentation for any purpose is hereby granted without fee, provided that
+ * the above copyright notice appear in all copies and that both that
+ * copyright notice and this permission notice appear in supporting
+ * documentation. No representations are made about the suitability of this
+ * software for any purpose. It is provided "as is" without express or
+ * implied warranty.
+ */
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+
+#ifndef ESRCH
+# include
+#endif
+
+#include /* sys/resource.h needs this for timeval */
+# include /* for waitpid() and associated macros */
+
+#ifdef VMS
+# include
+# include /* for close */
+# include /* for getpid */
+# define pid_t int
+# define fork vfork
+#endif /* VMS */
+
+#include /* for the signal names */
+
+#include
+#include "subprocs.h"
+
+#if !defined(SIGCHLD) && defined(SIGCLD)
+# define SIGCHLD SIGCLD
+#endif
+
+/* Semaphore to temporarily turn the SIGCHLD handler into a no-op.
+ Don't alter this directly -- use block_sigchld() / unblock_sigchld().
+*/
+static int block_sigchld_handler = 0;
+
+
+#ifdef HAVE_SIGACTION
+sigset_t
+#else /* !HAVE_SIGACTION */
+int
+#endif /* !HAVE_SIGACTION */
+block_sigchld (void)
+{
+#ifdef HAVE_SIGACTION
+ sigset_t child_set;
+ sigemptyset (&child_set);
+ sigaddset (&child_set, SIGCHLD);
+ sigaddset (&child_set, SIGPIPE);
+ sigprocmask (SIG_BLOCK, &child_set, 0);
+#endif /* HAVE_SIGACTION */
+
+ block_sigchld_handler++;
+
+#ifdef HAVE_SIGACTION
+ return child_set;
+#else /* !HAVE_SIGACTION */
+ return 0;
+#endif /* !HAVE_SIGACTION */
+}
+
+void
+unblock_sigchld (void)
+{
+#ifdef HAVE_SIGACTION
+ sigset_t child_set;
+ sigemptyset (&child_set);
+ sigaddset (&child_set, SIGCHLD);
+ sigaddset (&child_set, SIGPIPE);
+ sigprocmask (SIG_UNBLOCK, &child_set, 0);
+#endif /* HAVE_SIGACTION */
+
+ block_sigchld_handler--;
+}
+
+int
+signal_pid (int pid,
+ int signal)
+{
+ int status = -1;
+ gboolean verbose = TRUE;
+
+ if (block_sigchld_handler)
+ /* This function should not be called from the signal handler. */
+ abort();
+
+ block_sigchld (); /* we control the horizontal... */
+
+ status = kill (pid, signal);
+
+ if (verbose && status < 0) {
+ if (errno == ESRCH)
+ g_message ("Child process %lu was already dead.",
+ (unsigned long) pid);
+ else {
+ char buf [1024];
+ snprintf (buf, sizeof (buf), "Couldn't kill child process %lu",
+ (unsigned long) pid);
+ perror (buf);
+ }
+ }
+
+ unblock_sigchld ();
+
+ if (block_sigchld_handler < 0)
+ abort ();
+
+ return status;
+}
+
+#ifndef VMS
+
+void
+await_dying_children (int pid,
+ gboolean debug)
+{
+ while (1) {
+ int wait_status = 0;
+ pid_t kid;
+
+ errno = 0;
+ kid = waitpid (-1, &wait_status, WNOHANG|WUNTRACED);
+
+ if (debug) {
+ if (kid < 0 && errno)
+ g_message ("waitpid(%d) ==> %ld (%d)", pid, (long) kid, errno);
+ else if (kid != 0)
+ g_message ("waitpid(%d) ==> %ld", pid, (long) kid);
+ }
+
+ /* 0 means no more children to reap.
+ -1 means error -- except "interrupted system call" isn't a "real"
+ error, so if we get that, we should just try again. */
+ if (kid < 0 && errno != EINTR)
+ break;
+ }
+}
+
+
+#else /* VMS */
+static void await_dying_children (saver_info *si) { return; }
+#endif /* VMS */
+
diff --git a/src/screensaver/subprocs.h b/src/screensaver/subprocs.h
new file mode 100644
index 0000000000..30fec7b9a9
--- /dev/null
+++ b/src/screensaver/subprocs.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * subprocs.c --- choosing, spawning, and killing screenhacks.
+ *
+ * xscreensaver, Copyright (c) 1991-2003 Jamie Zawinski
+ *
+ * Permission to use, copy, modify, distribute, and sell this software and its
+ * documentation for any purpose is hereby granted without fee, provided that
+ * the above copyright notice appear in all copies and that both that
+ * copyright notice and this permission notice appear in supporting
+ * documentation. No representations are made about the suitability of this
+ * software for any purpose. It is provided "as is" without express or
+ * implied warranty.
+ */
+
+#ifndef __GS_SUBPROCS_H
+#define __GS_SUBPROCS_H
+
+#include
+
+G_BEGIN_DECLS
+
+void unblock_sigchld (void);
+
+#ifdef HAVE_SIGACTION
+sigset_t
+#else /* !HAVE_SIGACTION */
+int
+#endif /* !HAVE_SIGACTION */
+block_sigchld (void);
+
+int signal_pid (int pid,
+ int signal);
+void await_dying_children (int pid,
+ gboolean debug);
+
+G_END_DECLS
+
+#endif /* __GS_SUBPROCS_H */
diff --git a/src/st/st-button.c b/src/st/st-button.c
index 512c69fdcd..608047a92e 100644
--- a/src/st/st-button.c
+++ b/src/st/st-button.c
@@ -40,6 +40,7 @@
#include "st-button.h"
+#include "st-icon.h"
#include "st-enum-types.h"
#include "st-texture-cache.h"
#include "st-private.h"
@@ -51,6 +52,7 @@ enum
PROP_0,
PROP_LABEL,
+ PROP_ICON_NAME,
PROP_BUTTON_MASK,
PROP_TOGGLE_MODE,
PROP_CHECKED,
@@ -316,6 +318,9 @@ st_button_set_property (GObject *gobject,
case PROP_LABEL:
st_button_set_label (button, g_value_get_string (value));
break;
+ case PROP_ICON_NAME:
+ st_button_set_icon_name (button, g_value_get_string (value));
+ break;
case PROP_BUTTON_MASK:
st_button_set_button_mask (button, g_value_get_flags (value));
break;
@@ -346,6 +351,9 @@ st_button_get_property (GObject *gobject,
case PROP_LABEL:
g_value_set_string (value, priv->text);
break;
+ case PROP_ICON_NAME:
+ g_value_set_string (value, st_button_get_icon_name (ST_BUTTON (gobject)));
+ break;
case PROP_BUTTON_MASK:
g_value_set_flags (value, priv->button_mask);
break;
@@ -405,6 +413,12 @@ st_button_class_init (StButtonClass *klass)
NULL, G_PARAM_READWRITE);
g_object_class_install_property (gobject_class, PROP_LABEL, pspec);
+ pspec = g_param_spec_string ("icon-name",
+ "Icon name",
+ "Icon name of the button",
+ NULL, G_PARAM_READWRITE);
+ g_object_class_install_property (gobject_class, PROP_ICON_NAME, pspec);
+
pspec = g_param_spec_flags ("button-mask",
"Button mask",
"Which buttons trigger the 'clicked' signal",
@@ -553,6 +567,66 @@ st_button_set_label (StButton *button,
g_object_notify (G_OBJECT (button), "label");
}
+/**
+ * st_button_get_icon_name:
+ * @button: a #StButton
+ *
+ * Get the icon name of the button. If the button isn't showing an icon,
+ * the return value will be %NULL.
+ *
+ * Returns: (transfer none) (nullable): the icon name of the button
+ */
+const char *
+st_button_get_icon_name (StButton *button)
+{
+ ClutterActor *icon;
+
+ g_return_val_if_fail (ST_IS_BUTTON (button), NULL);
+
+ icon = st_bin_get_child (ST_BIN (button));
+ if (ST_IS_ICON (icon))
+ return st_icon_get_icon_name (ST_ICON (icon));
+ return NULL;
+}
+
+/**
+ * st_button_set_icon_name:
+ * @button: a #Stbutton
+ * @icon_name: an icon name
+ *
+ * Adds an `StIcon` with the given icon name as a child.
+ *
+ * If @button already contains a child actor, that child will
+ * be removed and replaced with the icon.
+ */
+void
+st_button_set_icon_name (StButton *button,
+ const char *icon_name)
+{
+ ClutterActor *icon;
+
+ g_return_if_fail (ST_IS_BUTTON (button));
+ g_return_if_fail (icon_name != NULL);
+
+ icon = st_bin_get_child (ST_BIN (button));
+
+ if (ST_IS_ICON (icon))
+ {
+ st_icon_set_icon_name (ST_ICON (icon), icon_name);
+ }
+ else
+ {
+ icon = g_object_new (ST_TYPE_ICON,
+ "icon-name", icon_name,
+ "x-align", CLUTTER_ACTOR_ALIGN_CENTER,
+ "y-align", CLUTTER_ACTOR_ALIGN_CENTER,
+ NULL);
+ st_bin_set_child (ST_BIN (button), icon);
+ }
+
+ g_object_notify (G_OBJECT (button), "icon-name");
+}
+
/**
* st_button_get_button_mask:
* @button: a #StButton
diff --git a/src/st/st-button.h b/src/st/st-button.h
index 1e55dfd6a7..3d0e98302c 100644
--- a/src/st/st-button.h
+++ b/src/st/st-button.h
@@ -73,6 +73,9 @@ StWidget *st_button_new_with_label (const gchar *text);
const gchar *st_button_get_label (StButton *button);
void st_button_set_label (StButton *button,
const gchar *text);
+const char *st_button_get_icon_name (StButton *button);
+void st_button_set_icon_name (StButton *button,
+ const char *icon_name);
void st_button_set_toggle_mode (StButton *button,
gboolean toggle);
gboolean st_button_get_toggle_mode (StButton *button);
diff --git a/src/st/st-password-entry.c b/src/st/st-password-entry.c
index a4a9965007..e527ebbf74 100644
--- a/src/st/st-password-entry.c
+++ b/src/st/st-password-entry.c
@@ -160,7 +160,7 @@ st_password_entry_init (StPasswordEntry *entry)
priv->peek_password_icon = g_object_new (ST_TYPE_ICON,
"style-class", "peek-password",
- "icon-name", "xsi-view-conceal-symbolic",
+ "icon-name", "xsi-view-reveal-symbolic",
NULL);
st_entry_set_secondary_icon (ST_ENTRY (entry), priv->peek_password_icon);
@@ -257,12 +257,12 @@ st_password_entry_set_password_visible (StPasswordEntry *entry,
if (priv->password_visible)
{
clutter_text_set_password_char (CLUTTER_TEXT (clutter_text), 0);
- st_icon_set_icon_name (ST_ICON (priv->peek_password_icon), "xsi-view-reveal-symbolic");
+ st_icon_set_icon_name (ST_ICON (priv->peek_password_icon), "xsi-view-conceal-symbolic");
}
else
{
clutter_text_set_password_char (CLUTTER_TEXT (clutter_text), BULLET);
- st_icon_set_icon_name (ST_ICON (priv->peek_password_icon), "xsi-view-conceal-symbolic");
+ st_icon_set_icon_name (ST_ICON (priv->peek_password_icon), "xsi-view-reveal-symbolic");
}
g_object_notify (G_OBJECT (entry), "password-visible");
diff --git a/src/st/st-texture-cache.c b/src/st/st-texture-cache.c
index 4438848550..868e3773bd 100644
--- a/src/st/st-texture-cache.c
+++ b/src/st/st-texture-cache.c
@@ -1690,7 +1690,6 @@ st_texture_cache_load_image_from_file_async (StTextureCache *ca
StTextureCacheLoadImageCallback callback,
gpointer user_data)
{
- gint scale;
if (callback == NULL)
{
g_warning ("st_texture_cache_load_image_from_file_async callback cannot be NULL");
@@ -1699,10 +1698,9 @@ st_texture_cache_load_image_from_file_async (StTextureCache *ca
ImageFromFileAsyncData *data;
GTask *result;
- scale = st_theme_context_get_scale_for_stage (),
data = g_new0 (ImageFromFileAsyncData, 1);
- data->width = width == -1 ? -1 : width * scale;
- data->height = height == -1 ? -1 : height * scale;
+ data->width = width;
+ data->height = height;
static gint handles = 1;
data->handle = handles++;
diff --git a/src/st/st-theme-context.c b/src/st/st-theme-context.c
index 26cf0f222d..c5f122537c 100644
--- a/src/st/st-theme-context.c
+++ b/src/st/st-theme-context.c
@@ -21,10 +21,12 @@
#include
+#include "st-border-image.h"
#include "st-settings.h"
#include "st-texture-cache.h"
#include "st-theme.h"
#include "st-theme-context.h"
+#include "st-theme-node-private.h"
struct _StThemeContext {
GObject parent;
@@ -68,6 +70,9 @@ static void on_font_name_changed (StSettings *settings,
StThemeContext *context);
static void on_icon_theme_changed (StTextureCache *cache,
StThemeContext *context);
+static void on_texture_file_changed (StTextureCache *cache,
+ GFile *file,
+ StThemeContext *context);
static void st_theme_context_changed (StThemeContext *context);
@@ -91,6 +96,9 @@ st_theme_context_finalize (GObject *object)
g_signal_handlers_disconnect_by_func (st_texture_cache_get_default (),
(gpointer) on_icon_theme_changed,
context);
+ g_signal_handlers_disconnect_by_func (st_texture_cache_get_default (),
+ (gpointer) on_texture_file_changed,
+ context);
g_signal_handlers_disconnect_by_func (clutter_get_default_backend (),
(gpointer) st_theme_context_changed,
@@ -152,6 +160,10 @@ st_theme_context_init (StThemeContext *context)
"icon-theme-changed",
G_CALLBACK (on_icon_theme_changed),
context);
+ g_signal_connect (st_texture_cache_get_default (),
+ "texture-file-changed",
+ G_CALLBACK (on_texture_file_changed),
+ context);
g_signal_connect_swapped (clutter_get_default_backend (),
"resolution-changed",
@@ -292,6 +304,50 @@ on_icon_theme_changed (StTextureCache *cache,
g_idle_add ((GSourceFunc) changed_idle, context);
}
+static void
+on_texture_file_changed (StTextureCache *cache,
+ GFile *file,
+ StThemeContext *context)
+{
+ GHashTableIter iter;
+ StThemeNode *node;
+ char *changed_path;
+
+ changed_path = g_file_get_path (file);
+ if (changed_path == NULL)
+ return;
+
+ g_hash_table_iter_init (&iter, context->nodes);
+ while (g_hash_table_iter_next (&iter, (gpointer *) &node, NULL))
+ {
+ const char *node_file;
+ StBorderImage *border_image;
+
+ node_file = st_theme_node_get_background_image (node);
+ if (node_file != NULL && strcmp (node_file, changed_path) == 0)
+ {
+ _st_theme_node_free_drawing_state (node);
+ node->alloc_width = 0;
+ node->alloc_height = 0;
+ continue;
+ }
+
+ border_image = st_theme_node_get_border_image (node);
+ if (border_image != NULL)
+ {
+ node_file = st_border_image_get_filename (border_image);
+ if (node_file != NULL && strcmp (node_file, changed_path) == 0)
+ {
+ _st_theme_node_free_drawing_state (node);
+ node->alloc_width = 0;
+ node->alloc_height = 0;
+ }
+ }
+ }
+
+ g_free (changed_path);
+}
+
/**
* st_theme_context_get_for_stage:
* @stage: a #ClutterStage