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 @@ - - - - - - False - 6 - dialog - - - True - False - vertical - 6 - - - True - False - end - - - gtk-cancel - False - True - True - True - True - - - False - False - 0 - - - - - gtk-ok - False - True - True - True - True - - - False - False - 1 - - - - - False - True - 3 - end - 0 - - - - - True - False - gtk-missing-image - - - False - True - 3 - 1 - - - - - True - False - - - - False - True - 3 - 2 - - - - - True - False - - - - - - True - False - - - - - - True - True - - False - False - - - True - True - 1 - - - - - - - - True - True - 1 - - - - - True - True - 3 - 3 - - - - - - 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