From 20514d46cd87b68565eec13cd4bd198f19d4701d Mon Sep 17 00:00:00 2001 From: fredcw <58893963+fredcw@users.noreply.github.com> Date: Tue, 3 Feb 2026 17:49:21 +0000 Subject: [PATCH] menu@cinnamon.org: Replace "Uninstall" context menu item with "App Info" "Uninstall" calls cinnamon-remove-application, a simple mint only script. Replace with "App Info" to open mintinstall or pamac-manager on the app details page so user can view an app's details and optionally uninstall using the OS's software manager. Revert "userWidget.js: Fix updating the image when the user avatar changes." This reverts commit 347c39cdf0a97841c2d3317b8c05e3743831d17d. The texture cache properly drops the image when the underlying file changes, but the theme context itself has created its own cogl resource and caches it also. We can monitor the texture cache there. st-theme-context.c: Hook up texture cache's texture-file-changed signal and invalidate any theme nodes using the passed file. If the underlying file contents changed, this forces a reload of the background/border image. main.js: Fix enum in _stageEventHandler. This is needed as a result of an introspection fix in muffin: linuxmint/muffin@7453e8de7689934 power_applet: rearrange battery label, so the percentage is on left (#13262) old: new: \t Co-authored-by: Mohamed A. Elmeligy theme: Add a couple of generic button styles (#13319) cinnamon-desktop-editor.py: Add import path. Since 0344b1138209a, cinnamon-menu-editor crashes at startup when it imports JsonSettingsWidgets. cinnamon-settings: Make imports consistent, instead of a mix of absolute and relative throughout. ref: 6d15343879693c, 0344b1138209ad messageTray.js: Fix cursor name. ref: 03e99902b013cf8c endSessionDialog.js: Don't close the dialog immediately when selecting 'suspend' or 'hibernate'. This was preventing the session manager from responding with an inhibitor list and giving the user the option to ignore them and 'force' the action, as happens correctly with other options. It was also breaking the lifecycle of this dialog and disable it from showing again. Also fix an issue with the inhibitor list not clearing existing items when receiving an updated list from the session manager. Fixes linuxmint/cinnamon-session#193. ui: Add a new placeholder object (#13304) Gives a simple reusable object to show when no active items are available. Make use of this new object in menu@cinnamon.org when the recent and favorite categories are empty. panel.js: Remove the panel corners implementation (#13487) This isn't used by anything and adds a lot of extra code complexity. st-button: Add an :icon-name property (#13491) Adds a convenient way to create an icon only button hotcorner.js: Port to GObject and ripples.js (#13544) We have had the ripples code duplicated for quite some time. Remove the duplicate code from here. Port to the hotcorners to GObject and clean up some variable naming and code styling. keybindings.js: Allow auto-repeat only on certain media keybindings. Use a new Meta.Display.add_custom_keybinding_full() to be able to specify Meta.KeyBindingFlags. The default flag via the old method was NONE, which enabled autorepeat for all keybindings. Make the new default IGNORE_AUTOREPEAT, with the exception of certain media keybindings. Xlet keybindings can make use of these flags with an extra argument in addXletHotKey() if they want to re-enable autorepeat. ref: linuxmint/muffin@baf3c23992b. menu@cinnamon.org: hide sidebar config options when sidebar is hidden (#13323) menu: Add context menu actions for favorites/recents (#13220) * menu: Add context menu actions for favorites/recents Add `Remove from favorites` and `Open containing folder` action for favorites/recents. * menu: Add fallback action for the `Open containing folder` FIX: Disable "Disable all" button when no extensions installed (#13455) Fixes issue where the "Disable all" button in Extensions settings remained enabled even when no extensions were installed. The button state is now correctly updated in update_button_states() to check if extension_rows is empty, and load_extensions() calls update_button_states() to ensure proper initialization. Fixes: linuxmint/cinnamon#13021 Fix wrong attribute and method name usage (#13515) checkBox.js: Use the new checkBox object (#13564) Remove all the old checkbox code and move to the use of the new one. Removes a use of CinnamonGenericContainer. This is hardly used in any spices so shouldn't have many side effects. Most themes should also just work. style: Add a slight blue tint to the theme (#13568) Many themes use this styling. Since people installing Cinnamon outside of will likely be using Adwaita or a something similar, tint the default theme a slight blue to fit in better with these other themes. Separator cleanup (#13573) * separator.js: Convert to GObject We only had one user of this in Cinnamon's codebase. Should be a pretty easy fix for any spice that uses it. Also removes the use of Lang.bind() * popupmenu.js: Use the standalone separator object We are duplicating the code for drawing the separator in object in menus for no real reason. Just use the the standalone separtor object instead. backgroundManager.js: Fix variable names and remove uses of Lang.bind() (#13574) Remove the edgeFlip feature (#13581) This has been unused for years. Go ahead and remove it. radioButton.js: Modernize and clean up unused object (#13567) Port the radio button to GObject. Removes a use of CinnamonGenericContainer. Nothing else in here seems to be used in either Cinnamon or any of the spices. This removes the radio group but this could be modernized and brought back if there is an actual call for it. st-texture-cache.c: Fix st_texture_cache_load_image_from_file_async. This was applying the scale factor to images even though in many places it's used this was already being done by its caller. This resulted in inconsistencies depending on how the returned actor was used. Remove the internal scaling and fix callers that were unknowingly relying on this behavior. cinnamonEntry: Modernize and clean up (#13571) Move to new style classes and remove all uses of Lang.bind. No functional changes. locatePointer: Fix and object name and cleanup (#13585) Properly case the LocatePointer object, remove usage of Lang.bind, and clean up unused imports Port Lightbox and Flashspot to GObject (#13586) The port requires renaming the show/hide methods of the Lightbox object. Update several other things while at it. Use connectObject, clean up some clutter deprecations, and use better comparison operators. Lightbox isn't really used in any spices, so this shouldn't have any real effect on them. lookingGlass.js: Port the inspector to GObject (#13589) This allows us to get rid of another use CinnamonGenericContainer. No functional changes layout.js: Port all objects to classes (#13590) While at it, remove all uses of Lang.bind and make use of connectObject for some of the signal tracking. github: Add pattern-check workflow. polkit: Show the reveal icon to reveal the password The button should reflect its action, not the status of the entry. This is better because: - It's a button, it should reflect what it does. - It aligns with the way this is done in other parts of Cinnamon, slick-greeter, Mint tools and Firefox - The default icon depicts an eye to reveal, that's an easier concept to grasp than a hidden status with an icon representing the negation of a simpler one. keyboardManager.js: Ensure the drawing area fills its parent size so the keyboard layout subscript can be drawn. Regression from c25b61d7f3a22eacc9. Add native screensaver (#13432) * Implement a screensaver. - Functional in wayland or x11 - Add a native 'away message' dialog - Unify 'switch-to-greeter' code from multiple sources - Add systemd/consolekit support for session states - Provide cinnamon-screensaver-command (still utilized by csd- power for pre-power-event activation). - Remove cinnamon-screensaver as a required session component. - Allow disabling of the internal screensaver to continue using cinnamon-screensaver. - Unify mpris code for the sound applet and albumArtWidget.js - Unify power code for the power applet and powerWidget.js TODO: - Wallpaper in wayland sessions (postponed until there is layer- shell support in muffin). Imported from cinnamon-screensaver: - PAM-related files, cinnamon-screensaver-pam-helper - cinnamon-screensaver-command and cinnamon-unlock-desktop: Both remain compatible with cinnamon-screensaver (> 6.7). The screenShield actor contains all other related widgets like the unlock dialog, and is placed in the new screenShieldGroup, the top- most child of the global.stage. Requires: https://github.com/linuxmint/muffin/pull/797 https://github.com/linuxmint/cinnamon-screensaver/pull/491 https://github.com/linuxmint/cinnamon-settings-daemon/pull/442 * Add backup-locker support for screensaver. Imported from cinnamon-screensaver, but improved... - Relies on dbus-activation now, which allows easier interaction between cinnamon and the locker process to coordinate grabs. - Add a simple dbus service to cinnamon-launcher, to allow restart from the backup locker (still falling back to tty instructions if the launcher isn't available). - Re-use event-grabber and event-filter from cinnamon-screensaver The backup window spawns when the screensaver shows, and is placed in global.top_window_group (where other override-redirect/POPUP-type windows are placed automatically). In the event of a crash/restart, the backup window will try to raise itself to the top, and will continue to until Cinnamon restarts. When Cinnamon restarts, if its stored state (gsettings) shows it should be locked, it will negotiate the modal grab from the backup locker, and immediately lock the screensaver again. * debian/control: Add Breaks/Replaces cinnamon-screensaver. * Restrict access to screenShield instance and screensaver service. unlockDialog, authClient: remove unnecessary import. cinnamon-screen.c: Update warnings. main.js: Only allow the internal screensaver for wayland sessions. extensions: Simplify code used for loading xlets, improve startup (#13479) speed, fix backtrace uselessness. - Drop 'custom' importer code, use native cjs importer - Leave compatibility functions for require() and module.exports Fixes/improves: - Faster Cinnamon startup (for my random configuration, ~1550ms down to ~1200ms). - Improved logging. - More consistent code (use of deprecated features will start getting flagged and/or blocked for PRs in Cinnamon and spice repositories. - Previous code was practically unmaintainable by sane people. Logging - where before we'd see this useless garbage: (cinnamon:4334): St-CRITICAL **: 22:19:57.081: st_widget_get_theme_node called on the widget [0x622f6579b3c0 StLabel.hourly-data ("...")] which is not in the stage. == Stack trace for context 0x622f63105b50 == 0 7ffd484001c0 b /usr/share/cinnamon/js/misc/fileUtils.js line 211 > Function:19086 (359f8e6c9c0 @ 279) 1 622f632b5d08 i /usr/share/cinnamon/js/misc/fileUtils.js line 211 > Function:19059 (359f8e6c970 @ 23) 2 7ffd48400ca0 b /usr/share/cinnamon/js/misc/fileUtils.js line 211 > Function:18976 (359f8e6c6a0 @ 807) 3 622f632b5b68 i /usr/share/cinnamon/js/misc/fileUtils.js line 211 > Function:19550 (359f8e6d650 @ 104) 4 622f632b5ab8 i /usr/share/cinnamon/js/misc/fileUtils.js line 211 > Function:19842 (359f8e6e2e0 @ 681) 5 622f632b5a08 i self-hosted:1461 (1eb5245b46f0 @ 30) 6 7ffd484015a0 b self-hosted:852 (359f8e92dd0 @ 15) we now get: (cinnamon:4334): St-CRITICAL **: 22:20:36.263: st_widget_get_theme_node called on the widget [0x58cf71ab5180 StLabel.hourly-data ("...")] which is not in the stage. == Stack trace for context 0x58cf6ee74250 == 0 7fffae294e10 b /home/mtwebster/.local/share/cinnamon/applets/weather@mockturtl/3.8/weather-applet.js:19084 (387d8e1fd1a0 @ 279) 1 58cf6eeac1f8 i /home/mtwebster/.local/share/cinnamon/applets/weather@mockturtl/3.8/weather-applet.js:19057 (387d8e1fd150 @ 23) 2 7fffae2958f0 b /home/mtwebster/.local/share/cinnamon/applets/weather@mockturtl/3.8/weather-applet.js:18974 (387d8e1fce20 @ 807) 3 58cf6eeac058 i /home/mtwebster/.local/share/cinnamon/applets/weather@mockturtl/3.8/weather-applet.js:19548 (387d8e1fddd0 @ 104) 4 58cf6eeabfa8 i /home/mtwebster/.local/share/cinnamon/applets/weather@mockturtl/3.8/weather-applet.js:19840 (387d8e1fea60 @ 681) 5 58cf6eeabef8 i self-hosted:1461 (2daaca0bf470 @ 30) 6 7fffae2961f0 b self-hosted:852 (e85adf8c6a0 @ 15) Requires https://github.com/linuxmint/cjs/pull/136 for xlet 'reload' functionality to work. popupMenu.js: Fix copy/paste error from 731d2f70fa. --- .github/workflows/build.yml | 1 - .github/workflows/pattern-checker.yml | 25 + cinnamon.session.in | 2 +- cinnamon2d.session.in | 2 +- data/meson.build | 2 + data/org.cinnamon.gschema.xml | 29 +- data/pam/cinnamon.pam | 16 + data/pam/cinnamon.pam.debian | 2 + data/pam/meson.build | 18 + data/services/meson.build | 1 + .../org.cinnamon.BackupLocker.service.in | 3 + data/theme/add-workspace-hover.svg | 8 +- data/theme/add-workspace.svg | 10 +- data/theme/checkbox-off.svg | 10 +- data/theme/checkbox.svg | 10 +- data/theme/cinnamon-sass/_colors.scss | 16 +- data/theme/cinnamon-sass/_widgets.scss | 2 + data/theme/cinnamon-sass/widgets/_base.scss | 22 +- .../theme/cinnamon-sass/widgets/_buttons.scss | 21 + .../theme/cinnamon-sass/widgets/_dialogs.scss | 2 +- data/theme/cinnamon-sass/widgets/_menus.scss | 5 + .../cinnamon-sass/widgets/_screensaver.scss | 199 +++ .../cinnamon-sass/widgets/_switch-check.scss | 19 +- data/theme/radio-off.svg | 8 +- data/theme/radio.svg | 8 +- data/theme/toggle-off.svg | 8 +- debian/cinnamon.install | 1 + debian/control | 7 +- debian/rules | 3 +- docs/reference/cinnamon/meson.build | 1 + files/usr/bin/cinnamon-install-spice | 4 +- files/usr/bin/cinnamon-launcher | 54 + files/usr/bin/cinnamon-screensaver-command | 3 + .../usr/bin/cinnamon-screensaver-lock-dialog | 8 - .../applets/calendar@cinnamon.org/applet.js | 6 +- .../applets/calendar@cinnamon.org/calendar.js | 2 +- .../calendar@cinnamon.org/eventView.js | 4 +- .../appGroup.js | 11 +- .../applet.js | 15 +- .../constants.js | 51 +- .../grouped-window-list@cinnamon.org/menus.js | 15 +- .../grouped-window-list@cinnamon.org/state.js | 6 +- .../workspace.js | 11 +- .../applets/keyboard@cinnamon.org/applet.js | 2 +- .../applets/menu@cinnamon.org/appUtils.js | 68 + .../applets/menu@cinnamon.org/applet.js | 255 +++- .../menu@cinnamon.org/settings-schema.json | 15 +- .../applets/power@cinnamon.org/applet.js | 154 +-- .../applets/sound@cinnamon.org/applet.js | 143 +- .../applets/user@cinnamon.org/applet.js | 52 +- .../xapp-status@cinnamon.org/applet.js | 5 +- .../cinnamon-desktop-editor.py | 4 +- .../cinnamon-screensaver-command.py | 200 +++ .../cinnamon-screensaver-lock-dialog.py | 68 - .../cinnamon-screensaver-lock-dialog.ui | 134 -- .../bin/AddKeyboardLayout.py | 2 +- .../cinnamon-settings/bin/ExtensionCore.py | 6 +- .../cinnamon-settings/bin/InputSources.py | 4 +- .../bin/JsonSettingsWidgets.py | 6 +- .../cinnamon-settings/bin/SettingsWidgets.py | 6 +- .../cinnamon/cinnamon-settings/bin/Spices.py | 2 +- .../cinnamon-settings/bin/TreeListWidgets.py | 2 +- .../cinnamon-settings/bin/test-add-layout | 6 +- .../cinnamon-settings/cinnamon-settings.py | 2 - .../modules/cs_accessibility.py | 2 +- .../cinnamon-settings/modules/cs_actions.py | 6 +- .../cinnamon-settings/modules/cs_applets.py | 6 +- .../modules/cs_backgrounds.py | 2 +- .../cinnamon-settings/modules/cs_calendar.py | 4 +- .../cinnamon-settings/modules/cs_default.py | 2 +- .../cinnamon-settings/modules/cs_desklets.py | 6 +- .../cinnamon-settings/modules/cs_desktop.py | 2 +- .../cinnamon-settings/modules/cs_display.py | 2 +- .../cinnamon-settings/modules/cs_effects.py | 2 +- .../modules/cs_extensions.py | 6 +- .../cinnamon-settings/modules/cs_fonts.py | 2 +- .../cinnamon-settings/modules/cs_general.py | 2 +- .../cinnamon-settings/modules/cs_gestures.py | 2 +- .../cinnamon-settings/modules/cs_hotcorner.py | 2 +- .../cinnamon-settings/modules/cs_info.py | 2 +- .../cinnamon-settings/modules/cs_keyboard.py | 4 +- .../cinnamon-settings/modules/cs_mouse.py | 2 +- .../modules/cs_nightlight.py | 2 +- .../modules/cs_notifications.py | 2 +- .../cinnamon-settings/modules/cs_panel.py | 2 +- .../cinnamon-settings/modules/cs_power.py | 2 +- .../cinnamon-settings/modules/cs_privacy.py | 2 +- .../modules/cs_screensaver.py | 10 +- .../cinnamon-settings/modules/cs_sound.py | 2 +- .../cinnamon-settings/modules/cs_startup.py | 2 +- .../cinnamon-settings/modules/cs_themes.py | 10 +- .../modules/cs_thunderbolt.py | 2 +- .../cinnamon-settings/modules/cs_user.py | 4 +- .../cinnamon-settings/modules/cs_windows.py | 2 +- .../modules/cs_workspaces.py | 2 +- .../cinnamon-settings/xlet-settings.py | 4 +- generate_cs_module_desktop_files.py | 1 - js/misc/authClient.js | 238 ++++ js/misc/config.js.in | 2 + js/misc/fileUtils.js | 212 --- js/misc/loginManager.js | 309 +++++ js/misc/mprisPlayer.js | 627 +++++++++ js/misc/powerUtils.js | 218 +++ js/misc/screenSaver.js | 122 +- js/misc/util.js | 92 ++ js/ui/appSwitcher/appSwitcher.js | 4 +- js/ui/appletManager.js | 6 +- js/ui/backgroundManager.js | 49 +- js/ui/checkBox.js | 156 +-- js/ui/cinnamonEntry.js | 80 +- js/ui/deskletManager.js | 6 +- js/ui/dnd.js | 2 +- js/ui/edgeFlip.js | 73 - js/ui/endSessionDialog.js | 11 +- js/ui/expo.js | 4 +- js/ui/extension.js | 514 +++++-- js/ui/extensionSystem.js | 11 +- js/ui/flashspot.js | 26 +- js/ui/hotCorner.js | 293 ++-- js/ui/keybindings.js | 99 +- js/ui/keyboardManager.js | 43 +- js/ui/keyringPrompt.js | 2 +- js/ui/layout.js | 352 +++-- js/ui/lightbox.js | 129 +- js/ui/locatePointer.js | 7 +- js/ui/lookingGlass.js | 59 +- js/ui/main.js | 165 ++- js/ui/messageTray.js | 2 +- js/ui/modalDialog.js | 7 +- js/ui/overview.js | 4 +- js/ui/panel.js | 592 +------- js/ui/placeholder.js | 100 ++ js/ui/popupMenu.js | 67 +- js/ui/radioButton.js | 211 +-- js/ui/screensaver/albumArtWidget.js | 631 +++++++++ js/ui/screensaver/awayMessageDialog.js | 68 + js/ui/screensaver/clockWidget.js | 126 ++ js/ui/screensaver/infoPanel.js | 119 ++ js/ui/screensaver/nameBlocker.js | 53 + js/ui/screensaver/notificationWidget.js | 102 ++ js/ui/screensaver/powerWidget.js | 167 +++ js/ui/screensaver/screenShield.js | 1229 +++++++++++++++++ js/ui/screensaver/screensaverWidget.js | 90 ++ js/ui/screensaver/unlockDialog.js | 430 ++++++ js/ui/screenshot.js | 4 +- js/ui/searchProviderManager.js | 3 +- js/ui/separator.js | 26 +- js/ui/userWidget.js | 120 +- js/ui/virtualKeyboard.js | 110 +- js/ui/windowManager.js | 13 +- js/ui/windowMenu.js | 22 +- meson.build | 48 + meson_options.txt | 10 + src/cinnamon-global.c | 17 + src/cinnamon-global.h | 2 + src/cinnamon-screen.c | 20 +- src/meson.build | 2 + src/screensaver/backup-locker/backup-locker.c | 1178 ++++++++++++++++ .../backup-locker/cinnamon-unlock-desktop | 7 + src/screensaver/backup-locker/event-grabber.c | 654 +++++++++ src/screensaver/backup-locker/event-grabber.h | 59 + .../backup-locker/gdk-event-filter-x11.c | 279 ++++ .../backup-locker/gdk-event-filter.h | 20 + src/screensaver/backup-locker/meson.build | 31 + .../cinnamon-screensaver-pam-helper.c | 579 ++++++++ src/screensaver/cs-auth-pam.c | 789 +++++++++++ src/screensaver/cs-auth.h | 67 + src/screensaver/meson.build | 26 + src/screensaver/setuid.c | 245 ++++ src/screensaver/setuid.h | 27 + src/screensaver/subprocs.c | 159 +++ src/screensaver/subprocs.h | 39 + src/st/st-button.c | 74 + src/st/st-button.h | 3 + src/st/st-password-entry.c | 6 +- src/st/st-texture-cache.c | 6 +- src/st/st-theme-context.c | 56 + 177 files changed, 11581 insertions(+), 2921 deletions(-) create mode 100644 .github/workflows/pattern-checker.yml create mode 100644 data/pam/cinnamon.pam create mode 100644 data/pam/cinnamon.pam.debian create mode 100644 data/pam/meson.build create mode 100644 data/services/org.cinnamon.BackupLocker.service.in create mode 100644 data/theme/cinnamon-sass/widgets/_buttons.scss create mode 100644 data/theme/cinnamon-sass/widgets/_screensaver.scss create mode 100755 files/usr/bin/cinnamon-screensaver-command delete mode 100755 files/usr/bin/cinnamon-screensaver-lock-dialog create mode 100755 files/usr/share/cinnamon/cinnamon-screensaver-command/cinnamon-screensaver-command.py delete mode 100755 files/usr/share/cinnamon/cinnamon-screensaver-lock-dialog/cinnamon-screensaver-lock-dialog.py delete mode 100644 files/usr/share/cinnamon/cinnamon-screensaver-lock-dialog/cinnamon-screensaver-lock-dialog.ui create mode 100644 js/misc/authClient.js create mode 100644 js/misc/loginManager.js create mode 100644 js/misc/mprisPlayer.js create mode 100644 js/misc/powerUtils.js delete mode 100644 js/ui/edgeFlip.js create mode 100644 js/ui/placeholder.js create mode 100644 js/ui/screensaver/albumArtWidget.js create mode 100644 js/ui/screensaver/awayMessageDialog.js create mode 100644 js/ui/screensaver/clockWidget.js create mode 100644 js/ui/screensaver/infoPanel.js create mode 100644 js/ui/screensaver/nameBlocker.js create mode 100644 js/ui/screensaver/notificationWidget.js create mode 100644 js/ui/screensaver/powerWidget.js create mode 100644 js/ui/screensaver/screenShield.js create mode 100644 js/ui/screensaver/screensaverWidget.js create mode 100644 js/ui/screensaver/unlockDialog.js create mode 100644 src/screensaver/backup-locker/backup-locker.c create mode 100644 src/screensaver/backup-locker/cinnamon-unlock-desktop create mode 100644 src/screensaver/backup-locker/event-grabber.c create mode 100644 src/screensaver/backup-locker/event-grabber.h create mode 100644 src/screensaver/backup-locker/gdk-event-filter-x11.c create mode 100644 src/screensaver/backup-locker/gdk-event-filter.h create mode 100644 src/screensaver/backup-locker/meson.build create mode 100644 src/screensaver/cinnamon-screensaver-pam-helper.c create mode 100644 src/screensaver/cs-auth-pam.c create mode 100644 src/screensaver/cs-auth.h create mode 100644 src/screensaver/meson.build create mode 100644 src/screensaver/setuid.c create mode 100644 src/screensaver/setuid.h create mode 100644 src/screensaver/subprocs.c create mode 100644 src/screensaver/subprocs.h 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